refactor: repo (#1070)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { MILLISECONDS_IN_A_DAY } from '@/Constants/Constants'
|
||||
|
||||
export const calculateDifferenceBetweenDatesInDays = (firstDate: Date, secondDate: Date) => {
|
||||
const firstDateAsUTCMilliseconds = Date.UTC(firstDate.getFullYear(), firstDate.getMonth(), firstDate.getDate())
|
||||
|
||||
const secondDateAsUTCMilliseconds = Date.UTC(secondDate.getFullYear(), secondDate.getMonth(), secondDate.getDate())
|
||||
|
||||
return Math.round((firstDateAsUTCMilliseconds - secondDateAsUTCMilliseconds) / MILLISECONDS_IN_A_DAY)
|
||||
}
|
||||
64
packages/web/src/javascripts/Utils/CalculateSubmenuStyle.tsx
Normal file
64
packages/web/src/javascripts/Utils/CalculateSubmenuStyle.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants'
|
||||
|
||||
export type SubmenuStyle = {
|
||||
top?: number | 'auto'
|
||||
right?: number | 'auto'
|
||||
bottom: number | 'auto'
|
||||
left?: number | 'auto'
|
||||
visibility?: 'hidden' | 'visible'
|
||||
maxHeight: number | 'auto'
|
||||
}
|
||||
|
||||
export const calculateSubmenuStyle = (
|
||||
button: HTMLButtonElement | null,
|
||||
menu?: HTMLDivElement | HTMLMenuElement | null,
|
||||
): SubmenuStyle | undefined => {
|
||||
const defaultFontSize = window.getComputedStyle(document.documentElement).fontSize
|
||||
const maxChangeEditorMenuSize = parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER
|
||||
const { clientWidth, clientHeight } = document.documentElement
|
||||
const buttonRect = button?.getBoundingClientRect()
|
||||
const buttonParentRect = button?.parentElement?.getBoundingClientRect()
|
||||
const menuBoundingRect = menu?.getBoundingClientRect()
|
||||
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
|
||||
const footerHeightInPx = footerElementRect?.height ?? 0
|
||||
|
||||
let position: SubmenuStyle = {
|
||||
bottom: 'auto',
|
||||
maxHeight: 'auto',
|
||||
}
|
||||
|
||||
if (buttonRect && buttonParentRect) {
|
||||
let positionBottom = clientHeight - buttonRect.bottom - buttonRect.height / 2
|
||||
|
||||
if (positionBottom < footerHeightInPx) {
|
||||
positionBottom = footerHeightInPx + MENU_MARGIN_FROM_APP_BORDER
|
||||
}
|
||||
|
||||
position = {
|
||||
bottom: positionBottom,
|
||||
visibility: 'hidden',
|
||||
maxHeight: 'auto',
|
||||
}
|
||||
|
||||
if (buttonRect.right + maxChangeEditorMenuSize > clientWidth) {
|
||||
position.right = clientWidth - buttonRect.left
|
||||
} else {
|
||||
position.left = buttonRect.right
|
||||
}
|
||||
}
|
||||
|
||||
if (menuBoundingRect?.height && buttonRect && position.bottom !== 'auto') {
|
||||
position.visibility = 'visible'
|
||||
|
||||
if (menuBoundingRect.y < MENU_MARGIN_FROM_APP_BORDER) {
|
||||
position.bottom = position.bottom + menuBoundingRect.y - MENU_MARGIN_FROM_APP_BORDER * 2
|
||||
}
|
||||
|
||||
if (footerElementRect && menuBoundingRect.height > footerElementRect.y) {
|
||||
position.bottom = footerElementRect.height + MENU_MARGIN_FROM_APP_BORDER
|
||||
position.maxHeight = clientHeight - footerElementRect.height - MENU_MARGIN_FROM_APP_BORDER * 2
|
||||
}
|
||||
}
|
||||
|
||||
return position
|
||||
}
|
||||
12
packages/web/src/javascripts/Utils/ConcatenateUint8Arrays.ts
Normal file
12
packages/web/src/javascripts/Utils/ConcatenateUint8Arrays.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const concatenateUint8Arrays = (arrays: Uint8Array[]) => {
|
||||
const totalLength = arrays.map((array) => array.length).reduce((prev, next) => prev + next, 0)
|
||||
|
||||
const concatenatedArray = new Uint8Array(totalLength)
|
||||
let offset = 0
|
||||
arrays.forEach((array) => {
|
||||
concatenatedArray.set(array, offset)
|
||||
offset += array.length
|
||||
})
|
||||
|
||||
return concatenatedArray
|
||||
}
|
||||
31
packages/web/src/javascripts/Utils/DragTypeCheck.ts
Normal file
31
packages/web/src/javascripts/Utils/DragTypeCheck.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
|
||||
const isBackupRelatedFile = (item: DataTransferItem, application: WebApplication): boolean => {
|
||||
const fileName = item.getAsFile()?.name || ''
|
||||
const isBackupMetadataFile = application.files.isFileNameFileBackupRelated(fileName) !== false
|
||||
return isBackupMetadataFile
|
||||
}
|
||||
|
||||
export const isHandlingFileDrag = (event: DragEvent, application: WebApplication) => {
|
||||
const items = event.dataTransfer?.items
|
||||
|
||||
if (!items) {
|
||||
return false
|
||||
}
|
||||
|
||||
return Array.from(items).some((item) => {
|
||||
return item.kind === 'file' && !isBackupRelatedFile(item, application)
|
||||
})
|
||||
}
|
||||
|
||||
export const isHandlingBackupDrag = (event: DragEvent, application: WebApplication) => {
|
||||
const items = event.dataTransfer?.items
|
||||
|
||||
if (!items) {
|
||||
return false
|
||||
}
|
||||
|
||||
return Array.from(items).every((item) => {
|
||||
return item.kind === 'file' && isBackupRelatedFile(item, application)
|
||||
})
|
||||
}
|
||||
5
packages/web/src/javascripts/Utils/FormatLastSyncDate.ts
Normal file
5
packages/web/src/javascripts/Utils/FormatLastSyncDate.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { dateToLocalizedString } from '@standardnotes/snjs/'
|
||||
|
||||
export const formatLastSyncDate = (lastUpdatedDate: Date) => {
|
||||
return dateToLocalizedString(lastUpdatedDate)
|
||||
}
|
||||
28
packages/web/src/javascripts/Utils/IsMobile.ts
Normal file
28
packages/web/src/javascripts/Utils/IsMobile.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* source: https://github.com/juliangruber/is-mobile
|
||||
*
|
||||
* (MIT)
|
||||
* Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
const mobileRE =
|
||||
/(android|bb\d+|meego).+mobile|armv7l|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i
|
||||
|
||||
const tabletRE = /android|ipad|playbook|silk/i
|
||||
|
||||
export type Opts = {
|
||||
tablet?: boolean
|
||||
}
|
||||
|
||||
export const isMobile = (opts: Opts = {}) => {
|
||||
const ua = navigator.userAgent || navigator.vendor
|
||||
|
||||
if (typeof ua !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return mobileRE.test(ua) || (!!opts.tablet && tabletRE.test(ua))
|
||||
}
|
||||
13
packages/web/src/javascripts/Utils/ManageSubscription.ts
Normal file
13
packages/web/src/javascripts/Utils/ManageSubscription.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { SNApplication } from '@standardnotes/snjs'
|
||||
|
||||
export function openSubscriptionDashboard(application: SNApplication): void {
|
||||
application
|
||||
.getNewSubscriptionToken()
|
||||
.then((token) => {
|
||||
if (!token) {
|
||||
return
|
||||
}
|
||||
window.open(`${window.dashboardUrl}?subscription_token=${token}`)
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
14
packages/web/src/javascripts/Utils/SortThemes.ts
Normal file
14
packages/web/src/javascripts/Utils/SortThemes.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ThemeItem } from '@/Components/QuickSettingsMenu/ThemeItem'
|
||||
|
||||
export const sortThemes = (a: ThemeItem, b: ThemeItem) => {
|
||||
const aIsLayerable = a.component?.isLayerable()
|
||||
const bIsLayerable = b.component?.isLayerable()
|
||||
|
||||
if (aIsLayerable && !bIsLayerable) {
|
||||
return 1
|
||||
} else if (!aIsLayerable && bIsLayerable) {
|
||||
return -1
|
||||
} else {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1
|
||||
}
|
||||
}
|
||||
61
packages/web/src/javascripts/Utils/StringUtils.spec.ts
Normal file
61
packages/web/src/javascripts/Utils/StringUtils.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { getIndexOfQueryInString, splitQueryInString, splitRangeWithinString } from './StringUtils'
|
||||
|
||||
describe('string utils', () => {
|
||||
describe('splitRangeWithinString', () => {
|
||||
it('should return whole string if range is invalid or out of bounds', () => {
|
||||
const string = 'test-string'
|
||||
|
||||
const outOfBoundsStartResult = splitRangeWithinString(string, 15, 0)
|
||||
expect(outOfBoundsStartResult).toStrictEqual(['test-string'])
|
||||
|
||||
const outOfBoundsEndResult = splitRangeWithinString(string, 0, -15)
|
||||
expect(outOfBoundsEndResult).toStrictEqual(['test-string'])
|
||||
|
||||
const invalidRangeResult = splitRangeWithinString(string, 15, 0)
|
||||
expect(invalidRangeResult).toStrictEqual(['test-string'])
|
||||
})
|
||||
|
||||
it('should return split string if range is valid', () => {
|
||||
const string = 'test-string'
|
||||
|
||||
const case1 = splitRangeWithinString(string, 0, 3)
|
||||
expect(case1).toStrictEqual(['tes', 't-string'])
|
||||
|
||||
const case2 = splitRangeWithinString(string, 2, 6)
|
||||
expect(case2).toStrictEqual(['te', 'st-s', 'tring'])
|
||||
|
||||
const case3 = splitRangeWithinString(string, 4, 9)
|
||||
expect(case3).toStrictEqual(['test', '-stri', 'ng'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getIndexOfQueryInString', () => {
|
||||
it('should get correct index of query in string', () => {
|
||||
const string = 'tEsT-sTrInG'
|
||||
|
||||
const indexOfQuery1 = getIndexOfQueryInString(string, 'tRi')
|
||||
expect(indexOfQuery1).toBe(6)
|
||||
|
||||
const indexOfQuery2 = getIndexOfQueryInString(string, 'StR')
|
||||
expect(indexOfQuery2).toBe(5)
|
||||
|
||||
const indexOfQuery3 = getIndexOfQueryInString(string, 'stringUtils')
|
||||
expect(indexOfQuery3).toBe(-1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('splitQueryInString', () => {
|
||||
it('should split string if it includes the query', () => {
|
||||
const string = 'TeSt-StRiNg'
|
||||
|
||||
const query1Result = splitQueryInString(string, 'T-sTr')
|
||||
expect(query1Result).toStrictEqual(['TeS', 't-StR', 'iNg'])
|
||||
|
||||
const query2Result = splitQueryInString(string, 'InG')
|
||||
expect(query2Result).toStrictEqual(['TeSt-StR', 'iNg'])
|
||||
|
||||
const query3Result = splitQueryInString(string, 'invalid query')
|
||||
expect(query3Result).toStrictEqual(['TeSt-StRiNg'])
|
||||
})
|
||||
})
|
||||
})
|
||||
27
packages/web/src/javascripts/Utils/StringUtils.ts
Normal file
27
packages/web/src/javascripts/Utils/StringUtils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export const splitRangeWithinString = (string: string, start: number, end: number) => {
|
||||
const isStartOutOfBounds = start > string.length || start < 0
|
||||
const isEndOutOfBounds = end > string.length || end < 0
|
||||
const isInvalidRange = start > end
|
||||
|
||||
if (isStartOutOfBounds || isEndOutOfBounds || isInvalidRange) {
|
||||
return [string]
|
||||
} else {
|
||||
return [string.slice(0, start), string.slice(start, end), string.slice(end)].filter((slice) => slice.length > 0)
|
||||
}
|
||||
}
|
||||
|
||||
export const getIndexOfQueryInString = (string: string, query: string) => {
|
||||
const lowercasedTitle = string.toLowerCase()
|
||||
const lowercasedQuery = query.toLowerCase()
|
||||
return lowercasedTitle.indexOf(lowercasedQuery)
|
||||
}
|
||||
|
||||
export const splitQueryInString = (string: string, query: string) => {
|
||||
const indexOfQueryInTitle = getIndexOfQueryInString(string, query)
|
||||
|
||||
if (indexOfQueryInTitle < 0) {
|
||||
return [string]
|
||||
}
|
||||
|
||||
return splitRangeWithinString(string, indexOfQueryInTitle, indexOfQueryInTitle + query.length)
|
||||
}
|
||||
172
packages/web/src/javascripts/Utils/Utils.ts
Normal file
172
packages/web/src/javascripts/Utils/Utils.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { Platform, platformFromString } from '@standardnotes/snjs'
|
||||
import { IsDesktopPlatform, IsWebPlatform } from '@/Constants/Version'
|
||||
import { EMAIL_REGEX } from '../Constants/Constants'
|
||||
export { isMobile } from './IsMobile'
|
||||
|
||||
declare const process: {
|
||||
env: {
|
||||
NODE_ENV: string | null | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
export function getPlatformString() {
|
||||
try {
|
||||
const platform = navigator.platform.toLowerCase()
|
||||
let trimmed = ''
|
||||
if (platform.includes('mac')) {
|
||||
trimmed = 'mac'
|
||||
} else if (platform.includes('win')) {
|
||||
trimmed = 'windows'
|
||||
} else if (platform.includes('linux')) {
|
||||
trimmed = 'linux'
|
||||
} else {
|
||||
/** Treat other platforms as linux */
|
||||
trimmed = 'linux'
|
||||
}
|
||||
return trimmed + (isDesktopApplication() ? '-desktop' : '-web')
|
||||
} catch (e) {
|
||||
return 'linux-web'
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlatform(): Platform {
|
||||
return platformFromString(getPlatformString())
|
||||
}
|
||||
|
||||
export function isSameDay(dateA: Date, dateB: Date): boolean {
|
||||
return (
|
||||
dateA.getFullYear() === dateB.getFullYear() &&
|
||||
dateA.getMonth() === dateB.getMonth() &&
|
||||
dateA.getDate() === dateB.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
/** Via https://davidwalsh.name/javascript-debounce-function */
|
||||
export function debounce(this: any, func: any, wait: number, immediate = false) {
|
||||
let timeout: NodeJS.Timeout | null
|
||||
return () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const context = this
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const args = arguments
|
||||
const later = function () {
|
||||
timeout = null
|
||||
if (!immediate) {
|
||||
func.apply(context, args)
|
||||
}
|
||||
}
|
||||
const callNow = immediate && !timeout
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
timeout = setTimeout(later, wait)
|
||||
if (callNow) {
|
||||
func.apply(context, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
|
||||
if (!Array.prototype.includes) {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Object.defineProperty(Array.prototype, 'includes', {
|
||||
value: function (searchElement: any, fromIndex: number) {
|
||||
if (this == null) {
|
||||
throw new TypeError('"this" is null or not defined')
|
||||
}
|
||||
|
||||
// 1. Let O be ? ToObject(this value).
|
||||
const o = Object(this)
|
||||
|
||||
// 2. Let len be ? ToLength(? Get(O, "length")).
|
||||
const len = o.length >>> 0
|
||||
|
||||
// 3. If len is 0, return false.
|
||||
if (len === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 4. Let n be ? ToInteger(fromIndex).
|
||||
// (If fromIndex is undefined, this step produces the value 0.)
|
||||
const n = fromIndex | 0
|
||||
|
||||
// 5. If n ≥ 0, then
|
||||
// a. Let k be n.
|
||||
// 6. Else n < 0,
|
||||
// a. Let k be len + n.
|
||||
// b. If k < 0, let k be 0.
|
||||
let k = Math.max(n >= 0 ? n : len - Math.abs(n), 0)
|
||||
|
||||
function sameValueZero(x: number, y: number) {
|
||||
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y))
|
||||
}
|
||||
|
||||
// 7. Repeat, while k < len
|
||||
while (k < len) {
|
||||
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
|
||||
// b. If SameValueZero(searchElement, elementK) is true, return true.
|
||||
if (sameValueZero(o[k], searchElement)) {
|
||||
return true
|
||||
}
|
||||
// c. Increase k by 1.
|
||||
k++
|
||||
}
|
||||
|
||||
// 8. Return false
|
||||
return false
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function preventRefreshing(message: string, operation: () => Promise<void> | void) {
|
||||
const onBeforeUnload = window.onbeforeunload
|
||||
try {
|
||||
window.onbeforeunload = () => message
|
||||
await operation()
|
||||
} finally {
|
||||
window.onbeforeunload = onBeforeUnload
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsWebPlatform && !IsDesktopPlatform) {
|
||||
throw Error('Neither __WEB__ nor __DESKTOP__ is true. Check your configuration files.')
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function destroyAllObjectProperties(object: any): void {
|
||||
for (const prop of Object.getOwnPropertyNames(object)) {
|
||||
try {
|
||||
delete object[prop]
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
|
||||
export function isDesktopApplication() {
|
||||
return IsDesktopPlatform
|
||||
}
|
||||
|
||||
export function getDesktopVersion() {
|
||||
return window.electronAppVersion
|
||||
}
|
||||
|
||||
export const isEmailValid = (email: string): boolean => {
|
||||
return EMAIL_REGEX.test(email)
|
||||
}
|
||||
|
||||
export const getWindowUrlParams = (): URLSearchParams => {
|
||||
return new URLSearchParams(window.location.search)
|
||||
}
|
||||
|
||||
export const openInNewTab = (url: string) => {
|
||||
const newWindow = window.open(url, '_blank', 'noopener,noreferrer')
|
||||
if (newWindow) {
|
||||
newWindow.opener = null
|
||||
}
|
||||
}
|
||||
|
||||
export const convertStringifiedBooleanToBoolean = (value: string) => {
|
||||
return value !== 'false'
|
||||
}
|
||||
5
packages/web/src/javascripts/Utils/index.ts
Normal file
5
packages/web/src/javascripts/Utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './CalculateSubmenuStyle'
|
||||
export * from './ConcatenateUint8Arrays'
|
||||
export * from './IsMobile'
|
||||
export * from './StringUtils'
|
||||
export * from './Utils'
|
||||
Reference in New Issue
Block a user