@knime/utils 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # ![Image](https://www.knime.com/sites/default/files/knime_logo_github_40x40_4layers.png) @knime/utils
2
+
3
+ Internal utility functions and constants shared across `@knime` packages.
4
+
5
+ ## Installation
6
+
7
+ To install the `@knime/utils` package, you can use npm:
8
+
9
+ ```bash
10
+ npm install @knime/utils
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ To use it in your project, you can import it as follows:
16
+
17
+ ```javascript
18
+ import { copyText } from "@knime/utils";
19
+ ```
20
+
21
+ # Join the Community!
22
+
23
+ - [KNIME Forum](https://forum.knime.com/)
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@knime/utils",
3
+ "version": "1.0.0",
4
+ "description": "Internal utility functions and constants shared across @knime packages.",
5
+ "homepage": "https://knime.github.io/webapps-common/",
6
+ "license": "GPL 3 and Additional Permissions according to Sec. 7 (SEE the file LICENSE)",
7
+ "author": "KNIME AG, Zurich, Switzerland",
8
+ "type": "module",
9
+ "scripts": {
10
+ "test:unit": "TZ='Europe/Berlin' vitest"
11
+ },
12
+ "dependencies": {
13
+ "@knime/styles": "1.0.0",
14
+ "consola": "2.15.3",
15
+ "date-fns": "2.30.0",
16
+ "date-fns-tz": "2.0.0"
17
+ },
18
+ "main": "src/index.ts",
19
+ "module": "src/index.ts",
20
+ "files": [
21
+ "src",
22
+ "!src/__tests__",
23
+ "test-utils",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "exports": {
27
+ ".": {
28
+ "import": "./src/index.ts"
29
+ },
30
+ "./globals.d.ts": {
31
+ "import": "./src/config/globals.d.ts"
32
+ },
33
+ "./test-utils": {
34
+ "import": "./test-utils/index.ts"
35
+ }
36
+ },
37
+ "devDependencies": {
38
+ "flush-promises": "^1.0.2"
39
+ }
40
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Uppercases first letter of string
3
+ * @param {String} str to be capitalized first string, e.g. 'workflow'
4
+ * @return {String} Capitalized String, e.g. Workflow
5
+ */
6
+ export const capitalize = (str) =>
7
+ str.charAt(0).toUpperCase() + str.substring(1);
8
+
9
+ const formats = {
10
+ customFormat: (str, delimiter) =>
11
+ capitalize(str.replace(delimiter, " ").toLowerCase()),
12
+ kebabFormat: (str) => formats.customFormat(str, "-"),
13
+ snakeFormat: (str) => formats.customFormat(str, "_"),
14
+ camelPascalCase: (str) =>
15
+ capitalize(
16
+ str
17
+ .match(/([A-Z]?[^A-Z]*)/g)
18
+ .slice(0, -1)
19
+ .join(" ")
20
+ .toLowerCase(),
21
+ ),
22
+ [undefined]: (str) => capitalize(str), // eslint-disable-line no-undefined
23
+ };
24
+
25
+ export const caseFormatter = ({ string, delimiter, format }) => {
26
+ if (delimiter) {
27
+ return formats.customFormat(string, delimiter);
28
+ }
29
+ if (formats) {
30
+ return formats[format](string);
31
+ }
32
+ return capitalize(string);
33
+ };
@@ -0,0 +1,3 @@
1
+ /* eslint-disable one-var */
2
+
3
+ declare const consola: import("consola").Consola;
@@ -0,0 +1,10 @@
1
+ export const copyText = (text) => {
2
+ let tmpElement = document.createElement("textarea");
3
+ tmpElement.style = "position:fixed;pointer-events:none;opacity:0;";
4
+ tmpElement.setAttribute("readonly", "");
5
+ tmpElement.value = text;
6
+ document.body.appendChild(tmpElement);
7
+ tmpElement.select();
8
+ document.execCommand("copy");
9
+ document.body.removeChild(tmpElement);
10
+ };
@@ -0,0 +1,57 @@
1
+ import { isAfter, isBefore } from "date-fns";
2
+ import updateTime from "./updateTime";
3
+ import updateDate from "./updateDate";
4
+
5
+ /**
6
+ * Check if date is before min.
7
+ * @param {Date} date
8
+ * @param {Date | null} min
9
+ * @param {Boolean} checkDate - check date part (year, day, month).
10
+ * @param {Boolean} checkTime - check time part (hour, minutes etc.).
11
+ * @returns {Boolean} whether limit is kept or not.
12
+ */
13
+ export const isBeforeMinDate = (date, min, checkDate, checkTime) => {
14
+ if (min) {
15
+ if (checkTime && checkDate) {
16
+ return isBefore(date, min);
17
+ }
18
+ if (checkTime) {
19
+ // only time is visible so ignore date: use same datre as base
20
+ const base = new Date(0);
21
+ return isBefore(updateTime(base, date), updateTime(base, min));
22
+ }
23
+ if (checkDate) {
24
+ // only date is visible so ignore time: use same time as base
25
+ const base = new Date(0);
26
+ return isBefore(updateDate(base, date), updateDate(base, min));
27
+ }
28
+ }
29
+ return false;
30
+ };
31
+
32
+ /**
33
+ * Check
34
+ * @param {Date} date - date to check.
35
+ * @param {Date | null} max - maximum date.
36
+ * @param {Boolean} checkDate - check date part (year, day, month).
37
+ * @param {Boolean} checkTime - check time part (hour, minutes etc.).
38
+ * @returns {Boolean} whether limit is kept or not.
39
+ */
40
+ export const isAfterMaxDate = (date, max, checkDate, checkTime) => {
41
+ if (max) {
42
+ if (checkTime && checkDate) {
43
+ return isAfter(date, max);
44
+ }
45
+ if (checkTime) {
46
+ // only time is visible so ignore date: use same datre as base
47
+ const base = new Date(0);
48
+ return isAfter(updateTime(base, date), updateTime(base, max));
49
+ }
50
+ if (checkDate) {
51
+ // use same time base
52
+ const base = new Date(0);
53
+ return isAfter(updateDate(base, date), updateDate(base, max));
54
+ }
55
+ }
56
+ return false;
57
+ };
@@ -0,0 +1,28 @@
1
+ // Use this method to reduce many method calls to a few.
2
+ // It's an 'immediate' implementation, so it will call the callbackindex: number once immediately an then again after the given wait.
3
+
4
+ type CallbackFunc<T> = (value: T, index: number) => void;
5
+
6
+ const debounce = <T>(callback: CallbackFunc<T>, wait: number) => {
7
+ let timer: ReturnType<typeof setTimeout>;
8
+ let lastCall = 0;
9
+
10
+ return (...args: Parameters<typeof callback>) => {
11
+ const [value, index] = args;
12
+ clearTimeout(timer);
13
+ const now = Date.now();
14
+ const timeFromLastCall = now - lastCall;
15
+
16
+ if (timeFromLastCall > wait) {
17
+ lastCall = now;
18
+ callback(value, index);
19
+ } else {
20
+ timer = setTimeout(() => {
21
+ lastCall = now;
22
+ callback(value, index);
23
+ }, wait);
24
+ }
25
+ };
26
+ };
27
+
28
+ export default debounce;
@@ -0,0 +1,41 @@
1
+ import csvIcon from "@knime/styles/img/icons/file-csv.svg";
2
+ import docxIcon from "@knime/styles/img/icons/file-docx.svg";
3
+ import htmlIcon from "@knime/styles/img/icons/file-html.svg";
4
+ import mdIcon from "@knime/styles/img/icons/file-md.svg";
5
+ import odpIcon from "@knime/styles/img/icons/file-odp.svg";
6
+ import odsIcon from "@knime/styles/img/icons/file-ods.svg";
7
+ import odtIcon from "@knime/styles/img/icons/file-odt.svg";
8
+ import pdfIcon from "@knime/styles/img/icons/file-pdf.svg";
9
+ import pptxIcon from "@knime/styles/img/icons/file-pptx.svg";
10
+ import psIcon from "@knime/styles/img/icons/file-ps.svg";
11
+ import xlsIcon from "@knime/styles/img/icons/file-xls.svg";
12
+ import xlsxIcon from "@knime/styles/img/icons/file-xlsx.svg";
13
+ import xmlIcon from "@knime/styles/img/icons/file-xml.svg";
14
+ import zipIcon from "@knime/styles/img/icons/file-zip.svg";
15
+ import exeIcon from "@knime/styles/img/icons/file-zip-exe.svg";
16
+ import txtIcon from "@knime/styles/img/icons/file-text.svg";
17
+ import fileIcon from "@knime/styles/img/icons/file-question.svg";
18
+
19
+ export const icons = {
20
+ csvIcon,
21
+ docxIcon,
22
+ htmlIcon,
23
+ mdIcon,
24
+ odpIcon,
25
+ odsIcon,
26
+ odtIcon,
27
+ pdfIcon,
28
+ pptxIcon,
29
+ psIcon,
30
+ xlsIcon,
31
+ xlsxIcon,
32
+ xmlIcon,
33
+ zipIcon,
34
+ exeIcon,
35
+ txtIcon,
36
+ fileIcon,
37
+ };
38
+
39
+ export const isIconExisting = function (iconName: string): boolean {
40
+ return icons.hasOwnProperty(iconName);
41
+ };
package/src/filters.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * These definitions can be used to filter an array. Each entry should contain a method normalize which can be used to
3
+ * compile any filter settings and a method "test" which takes the compiled result, additional settings and the item to
4
+ * be matched/filtered
5
+ * */
6
+ interface FilterDefinition<SearchTerm, Normalized, Testable> {
7
+ id: string;
8
+ normalize: (
9
+ searchTerm: SearchTerm,
10
+ caseSensitiveSearch: boolean,
11
+ ) => Normalized;
12
+ test: (
13
+ text: Testable,
14
+ normalizedSearchTerm: Normalized,
15
+ caseSensitiveSearch: boolean,
16
+ inverseSearch: boolean,
17
+ ) => boolean;
18
+ }
19
+
20
+ interface StringTest {
21
+ test(str: string): boolean;
22
+ }
23
+
24
+ type SearchFilterDefinition = FilterDefinition<string, string, string>;
25
+ type PatternFilterDefinition = FilterDefinition<
26
+ string,
27
+ RegExp | StringTest,
28
+ string
29
+ >;
30
+ type TypeFilterDefinition = FilterDefinition<
31
+ string[],
32
+ StringTest,
33
+ string | undefined
34
+ >;
35
+
36
+ const modeDefinitions = [
37
+ {
38
+ id: "search",
39
+ normalize(searchTerm, caseSensitiveSearch) {
40
+ return caseSensitiveSearch ? searchTerm : searchTerm.toLowerCase();
41
+ },
42
+ test(text, normalizedSearchTerm, caseSensitiveSearch, inverseSearch) {
43
+ const testText = caseSensitiveSearch ? text : text.toLowerCase();
44
+ const matches = testText.includes(normalizedSearchTerm);
45
+ return inverseSearch ? !matches : matches;
46
+ },
47
+ } satisfies SearchFilterDefinition,
48
+ {
49
+ id: "wildcard",
50
+ normalize(searchTerm, caseSensitiveSearch) {
51
+ // Do a regex search, explicitly matching start and end of the search term.
52
+ // All regex special character except from "* and ?" (wildcards) are escaped.
53
+ if (searchTerm.length > 0) {
54
+ const escapedSearchTerm = searchTerm.replace(
55
+ /[-[\]{}()+.,\\^$|#\s]/g,
56
+ "\\$&",
57
+ );
58
+ const wildcardSearchTerm = escapedSearchTerm
59
+ .replace(/\*/g, ".*")
60
+ .replace(/\?/g, ".?");
61
+ searchTerm = `^${wildcardSearchTerm}$`;
62
+ } else {
63
+ return { test: () => false };
64
+ }
65
+ try {
66
+ const flags = caseSensitiveSearch ? "" : "i";
67
+ return new RegExp(searchTerm, flags);
68
+ } catch (error) {
69
+ // In case of an invalid regular expression, an impossible
70
+ // regex is returned, not matching anything.
71
+ return /$^/;
72
+ }
73
+ },
74
+ test(text, normalizedSearchTerm, caseSensitiveSearch, inverseSearch) {
75
+ const matches = normalizedSearchTerm.test(text);
76
+ return inverseSearch ? !matches : matches;
77
+ },
78
+ } satisfies PatternFilterDefinition,
79
+ {
80
+ id: "regex",
81
+ normalize(searchTerm, caseSensitiveSearch) {
82
+ try {
83
+ const flags = caseSensitiveSearch ? "" : "i";
84
+ return new RegExp(`^(${searchTerm})$`, flags);
85
+ } catch (error) {
86
+ // In case of an invalid regular expression, an impossible
87
+ // regex is returned, not matching anything.
88
+ return /$^/;
89
+ }
90
+ },
91
+ test(text, normalizedSearchTerm, caseSensitiveSearch, inverseSearch) {
92
+ const matches = normalizedSearchTerm.test(text);
93
+ return inverseSearch ? !matches : matches;
94
+ },
95
+ } satisfies PatternFilterDefinition,
96
+ {
97
+ id: "type",
98
+ normalize(selectedTypes) {
99
+ return { test: (type: string) => selectedTypes.includes(type) };
100
+ },
101
+ test(type, normalizedSearchTerm) {
102
+ if (typeof type === "undefined") {
103
+ return false;
104
+ }
105
+ return normalizedSearchTerm.test(type);
106
+ },
107
+ } satisfies TypeFilterDefinition,
108
+ ];
109
+
110
+ // convert [{id: "key1", text: "asdf"}, ...] to {"key1": {id:"key1", text: "asdf"} ... }
111
+ export const filters: Record<
112
+ string,
113
+ FilterDefinition<any, any, any>
114
+ > = Object.assign({}, ...modeDefinitions.map((obj) => ({ [obj.id]: obj })));
package/src/format.js ADDED
@@ -0,0 +1,70 @@
1
+ import getLocalTimeZone from "./localTimezone";
2
+ /**
3
+ * Converts the specified date String to human readable text containing the day, month, and year.
4
+ * Example: '2018-07-31T09:44:31+00:00' => 'Jul 31, 2018'
5
+ *
6
+ * @param {Boolean} useTimeZone to set the timeZone where the user is located, and display local date and time
7
+ * @param {String | number} dateString String representation of the Date to format
8
+ * @returns {String} human readable date String
9
+ */
10
+ export const formatDateString = (dateString, useTimeZone = false) => {
11
+ let date = new Date(dateString);
12
+ if (isNaN(date)) {
13
+ throw Error("Invalid Date format");
14
+ }
15
+ // This only works for the default locale of en-US, as support of other locals in node js might not be available
16
+ // See for example this issue https://github.com/nodejs/node/issues/8500
17
+ let dateOptions = { day: "numeric", month: "short", year: "numeric" };
18
+ if (useTimeZone) {
19
+ dateOptions.timeZone = getLocalTimeZone();
20
+ }
21
+ return date.toLocaleDateString("en-US", dateOptions);
22
+ };
23
+ /**
24
+ * Converts the specified time String to human readable text containing the hour (12-hour) and minute.
25
+ * Example: '2018-07-31T09:44:31+00:00' => '9:44 AM'
26
+ *
27
+ * @param {Boolean} useTimeZone to set the timeZone where the user is located, and display local date and time
28
+ * @param {String | number} timeString String representation of the Date (including time) to format
29
+ * @returns {String} human readable time String
30
+ */
31
+ export const formatTimeString = (timeString, useTimeZone = false) => {
32
+ let time = new Date(timeString);
33
+ if (isNaN(time)) {
34
+ throw Error("Invalid Date format");
35
+ }
36
+ // This only works for the default locale of en-US, as support of other locals in node js might not be available
37
+ // See for example this issue https://github.com/nodejs/node/issues/8500
38
+
39
+ let timeOptions = { hour: "numeric", minute: "2-digit" };
40
+ if (useTimeZone) {
41
+ timeOptions.timeZone = getLocalTimeZone();
42
+ }
43
+
44
+ return time.toLocaleTimeString("en-US", timeOptions);
45
+ };
46
+ /**
47
+ * Converts the specified time String to human readable text containing the date (day, month, year) and time (hour,
48
+ * minute; 12-hour).
49
+ * Example: '2018-07-31T09:44:31+00:00' => 'Jul 31, 2018 9:44 AM'
50
+ *
51
+ * @param {Boolean} useTimeZone to set the timeZone where the user is located, and display local date and time
52
+ * @param {String | number} dateTimeString String representation of the Date (including time and date) to format
53
+ * @returns {String} human readable date/time String
54
+ */
55
+ export const formatDateTimeString = (dateTimeString, useTimeZone = false) => {
56
+ // eslint-disable-line arrow-body-style
57
+ return `${formatDateString(dateTimeString, useTimeZone)} ${formatTimeString(
58
+ dateTimeString,
59
+ useTimeZone,
60
+ )}`;
61
+ };
62
+ /**
63
+ * Converts the specified date and time to the user time zone with option to display (day, month, year) only or with time (hour,
64
+ * minute; 12-hour).
65
+ * @param {Boolean} showTime String representation of the Date to convert and format
66
+ * @param {String | number} date String representation of the Date to convert and format
67
+ * @returns {String} human readable date/time String converted to user time zone
68
+ */
69
+ export const formatLocalDateTimeString = (date, showTime) =>
70
+ showTime ? formatDateTimeString(date, true) : formatDateString(date, true);
@@ -0,0 +1,98 @@
1
+ import { isMac } from "./navigator";
2
+
3
+ type Keys =
4
+ | "F1"
5
+ | "F2"
6
+ | "F3"
7
+ | "F4"
8
+ | "F5"
9
+ | "F6"
10
+ | "F7"
11
+ | "F8"
12
+ | "F9"
13
+ | "F10"
14
+ | "F11"
15
+ | "F12"
16
+ | "PageUp"
17
+ | "PageDown"
18
+ | "ArrowUp"
19
+ | "ArrowRight"
20
+ | "ArrowDown"
21
+ | "ArrowLeft"
22
+ | "Delete"
23
+ | "Enter"
24
+ | "Backspace"
25
+ | "Home"
26
+ | "End"
27
+ | " "; // Space;
28
+
29
+ type Modifiers = "CtrlOrCmd" | "Ctrl" | "Alt" | "Shift";
30
+
31
+ // creates loose autocomplete by using a union of the passed-in type
32
+ // and the entire `string` set *except* the passed-in type
33
+ type LooseAutoComplete<T extends string> = T | Omit<string, T>;
34
+
35
+ export type NumberRange = `${number}-${number}`;
36
+ export type KnownHotkey = Keys | Modifiers | NumberRange;
37
+
38
+ export type Hotkey = LooseAutoComplete<Keys | Modifiers>;
39
+
40
+ const globalKeyMap = {
41
+ ArrowUp: "↑",
42
+ ArrowDown: "↓",
43
+ ArrowLeft: "←",
44
+ ArrowRight: "→",
45
+ Enter: "↵",
46
+ " ": "Space", // we use event.key and there space is an actual space not the code "Space"
47
+ CtrlOrCmd: "Ctrl",
48
+ } as const;
49
+
50
+ const MacOSkeyMap = {
51
+ Shift: "⇧",
52
+ Delete: "⌫",
53
+ CtrlOrCmd: "⌘",
54
+ Ctrl: "⌃",
55
+ Alt: "⌥",
56
+ Enter: "↩",
57
+ } as const;
58
+
59
+ type KeyFormatMap = typeof MacOSkeyMap | typeof globalKeyMap;
60
+
61
+ /**
62
+ * Returns a string representation of a single hotkey. Replaces some special key names with symbols
63
+ */
64
+ export const formatHotkey = (hotkey: Hotkey): string => {
65
+ /**
66
+ * Returns the symbol corresponding to the `key` based on the given
67
+ * `formatMap`. Otherwise, if not found, returns the same `key` value given
68
+ */
69
+ const mapSymbols = (formatMap: KeyFormatMap) => (key: Hotkey) =>
70
+ formatMap[key as keyof KeyFormatMap] ?? key;
71
+
72
+ /**
73
+ * Pipe function implementation. Receives a list of functions to execute in order
74
+ * and will pipe the parameter passed at the end to each function returning the final output
75
+ */
76
+ const pipe = <T extends () => any>(
77
+ ...fns: Array<(...args: any[]) => ReturnType<T>>
78
+ ) => fns.reduce((f, g) => (arg) => g(f(arg)));
79
+
80
+ const macKeyFormatter = pipe(
81
+ mapSymbols(MacOSkeyMap),
82
+ mapSymbols(globalKeyMap),
83
+ );
84
+
85
+ const generalKeyFormatter = mapSymbols(globalKeyMap);
86
+
87
+ return isMac() ? macKeyFormatter(hotkey) : generalKeyFormatter(hotkey);
88
+ };
89
+
90
+ export const getDefaultSeparator = () => " ";
91
+
92
+ /**
93
+ * Returns a string representation of a list of hotkeys. Replaces some special key names with symbols
94
+ */
95
+ export const formatHotkeys = (
96
+ hotkeys: Array<Hotkey>,
97
+ getSeparator = getDefaultSeparator,
98
+ ) => hotkeys.map(formatHotkey).join(getSeparator());
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Since (a % b) yields a negative outcome for negative a, we need this method in order
3
+ * to correctly compute wrapped around indices in lists
4
+ * @param {number} index an arbitrary integer
5
+ * @param {number} length a positive integer
6
+ * @returns {number} the non-negative modulo outcome
7
+ */
8
+ export default (index: number, length: number) =>
9
+ ((index % length) + length) % length;
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { capitalize, caseFormatter } from "./capitalize";
2
+ import { copyText } from "./copyText";
3
+ import debounce from "./debounce";
4
+ import { filters } from "./filters";
5
+ import truncateString from "./truncateString";
6
+ import getWrappedAroundIndex from "./getWrappedAroundIndex";
7
+ import sleep from "./sleep";
8
+ import { icons, isIconExisting } from "./fileTypeIcons";
9
+ import svgWithTitle from "./svgWithTitle";
10
+ import updateDate from "./updateDate";
11
+ import { isAfterMaxDate, isBeforeMinDate } from "./dateMinMaxCheck";
12
+ import {
13
+ formatDateString,
14
+ formatDateTimeString,
15
+ formatLocalDateTimeString,
16
+ formatTimeString,
17
+ } from "./format";
18
+
19
+ import getLocalTimeZone from "./localTimezone";
20
+ import numIntegerDigits from "./numIntegerDigits";
21
+ import { formatHotkeys } from "./formatHotkeys";
22
+ import type { Hotkey, KnownHotkey } from "./formatHotkeys";
23
+ import * as navigator from "./navigator";
24
+
25
+ export type { Hotkey, KnownHotkey };
26
+
27
+ export {
28
+ capitalize,
29
+ caseFormatter,
30
+ copyText,
31
+ debounce,
32
+ filters,
33
+ truncateString,
34
+ getWrappedAroundIndex,
35
+ sleep,
36
+ icons,
37
+ isIconExisting,
38
+ svgWithTitle,
39
+ formatDateString,
40
+ formatDateTimeString,
41
+ formatLocalDateTimeString,
42
+ formatTimeString,
43
+ updateDate,
44
+ isAfterMaxDate,
45
+ getLocalTimeZone,
46
+ isBeforeMinDate,
47
+ numIntegerDigits,
48
+ formatHotkeys,
49
+ navigator,
50
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Get the users local timezone.
3
+ *
4
+ * @returns {String} current users timezone (browser based) tz db string (e. g. Europe/Berlin).
5
+ */
6
+ // eslint-disable-next-line new-cap
7
+ export default () => Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -0,0 +1,11 @@
1
+ /**
2
+ *
3
+ * @returns true when the platform of the user is Mac
4
+ */
5
+ export const isMac = () => navigator?.userAgent?.toLowerCase()?.includes("mac");
6
+
7
+ /**
8
+ *
9
+ * @returns name of the primary modifier key for the current platform
10
+ */
11
+ export const getMetaOrCtrlKey = () => (isMac() ? "metaKey" : "ctrlKey");
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Count the number of integer digits for a given number.
3
+ *
4
+ * @param {Number} x - input number.
5
+ * @returns {Number} - number of integer digits x has. 1234 = 4 but 1234.44 is also 4.
6
+ */
7
+ export default (x) => Math.max(Math.floor(Math.log10(Math.abs(x))), 0) + 1;
package/src/sleep.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Returns promise that resolves after ms.
3
+ */
4
+ export default function sleep(ms: number) {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
@@ -0,0 +1,31 @@
1
+ import { h as createElement } from "vue";
2
+
3
+ /**
4
+ * Higher order component that decorates a SVG component (loaded with vue-svg-loader) with a `<title>` element
5
+ * Note that `title` attributes have no effect on `<svg>` elements, so we need to inject a DOM element.
6
+ *
7
+ * @example
8
+ *
9
+ * import MyIcon from '~/icons/foo.svg';
10
+ * import svgWithTitle from '…';
11
+ * export default {
12
+ * components: {
13
+ * MyIcon: svgWithTitle(MyIcon, 'My Title')
14
+ * }
15
+ * };
16
+ *
17
+ * @param { Vue.component } SvgComponent The component to decorate
18
+ * @param { String } title The title to insert
19
+ * @returns { Vue.component } The component with a title inserted as the first child of `<svg>`
20
+ */
21
+ export default (SvgComponent, title) => ({
22
+ render() {
23
+ const renderedComponent = SvgComponent.render.apply(this, createElement);
24
+
25
+ const titleEl = createElement("title", title);
26
+ titleEl.ns = "svg";
27
+
28
+ renderedComponent.children.unshift(titleEl);
29
+ return renderedComponent;
30
+ },
31
+ });
@@ -0,0 +1,7 @@
1
+ const defaultMaxLength = 160;
2
+
3
+ // truncates a string to a given max length
4
+ export default (string = "", maxLength = defaultMaxLength) => {
5
+ const dots = string.length > maxLength ? "…" : "";
6
+ return `${string.substring(0, maxLength - 1)}${dots}`;
7
+ };