diff --git a/.gitignore b/.gitignore index fd10abd9..e61b718d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store node_modules dist/ +.idea/ diff --git a/README.md b/README.md index 2469249c..aff79537 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ For more advanced, controlled typing effects, TypeIt comes with companion functi - Define strings programmatically or directly in the HTML (a useful fallback in case user doesn't have JavaScript enabled, as well as for SEO). - Handle HTML (even nested tags!) with ease, preserving all of its attributes (classes, ids, etc.). - Offered as an ES module for modern bundlers, or a UMD library for loading via a traditional ` diff --git a/packages/typeit/package-lock.json b/packages/typeit/package-lock.json index a32327dc..f1b11be4 100644 --- a/packages/typeit/package-lock.json +++ b/packages/typeit/package-lock.json @@ -1,18 +1,19 @@ { "name": "typeit", - "version": "8.7.0", + "version": "8.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "typeit", - "version": "8.7.0", + "version": "8.7.1", "hasInstallScript": true, "license": "GPL-3.0", "devDependencies": { "@babel/preset-env": "^7.20.2", "@babel/preset-typescript": "^7.18.6", "@types/web-animations-js": "^2.2.12", + "grapheme-splitter": "^1.0.4", "jest": "^29.3.1", "jest-cli": "^29.3.1", "jest-environment-jsdom": "^29.3.1", @@ -3969,6 +3970,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -10207,6 +10214,12 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", "dev": true }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", diff --git a/packages/typeit/package.json b/packages/typeit/package.json index bd9f1a16..52a8dfaa 100644 --- a/packages/typeit/package.json +++ b/packages/typeit/package.json @@ -42,6 +42,7 @@ "@babel/preset-env": "^7.20.2", "@babel/preset-typescript": "^7.18.6", "@types/web-animations-js": "^2.2.12", + "grapheme-splitter": "^1.0.4", "jest": "^29.3.1", "jest-cli": "^29.3.1", "jest-environment-jsdom": "^29.3.1", @@ -52,10 +53,12 @@ "jest": { "clearMocks": true, "testPathIgnorePatterns": [ - "/__tests__/setup.js" + "/__tests__/setup.js", + "/__tests__/helpers/util.js" ], "setupFilesAfterEnv": [ - "./__tests__/setup.js" + "./__tests__/setup.js", + "./__tests__/helpers/util.js" ], "testEnvironment": "jsdom" } diff --git a/packages/typeit/src/constants.ts b/packages/typeit/src/constants.ts index 968fa535..886dc763 100644 --- a/packages/typeit/src/constants.ts +++ b/packages/typeit/src/constants.ts @@ -1,4 +1,5 @@ import { CursorOptions, Options } from "./types"; +import toArray from "./helpers/toArray"; export const DATA_ATTRIBUTE = "data-typeit-id"; export const CURSOR_CLASS = "ti-cursor"; @@ -41,6 +42,7 @@ export const DEFAULT_OPTIONS: Options & { startDelay: 250, startDelete: false, strings: [], + stringSpliterator: (str) => toArray(str), waitUntilVisible: false, beforeString: () => {}, afterString: () => {}, diff --git a/packages/typeit/src/helpers/chunkStrings.ts b/packages/typeit/src/helpers/chunkStrings.ts index 470bde6a..58a05aeb 100644 --- a/packages/typeit/src/helpers/chunkStrings.ts +++ b/packages/typeit/src/helpers/chunkStrings.ts @@ -53,8 +53,11 @@ export function walkElementNodes( * Convert string to array of chunks that will be later * used to construct a TypeIt queue. */ -export function chunkStringAsHtml(string: string): El[] { - return walkElementNodes(getParsedBody(string)); +export function chunkStringAsHtml( + string: string, + stringSpliterator: (str: string) => string[] +): El[] { + return walkElementNodes(getParsedBody(string, stringSpliterator)); } /** @@ -63,11 +66,15 @@ export function chunkStringAsHtml(string: string): El[] { * * @param {string} str * @param {boolean} asHtml + * @param {(string) => string[]}stringSpliterator * @return {array} */ export function maybeChunkStringAsHtml( str: string, - asHtml = true + asHtml = true, + stringSpliterator: (str: string) => string[] ): Partial[] { - return asHtml ? chunkStringAsHtml(str) : toArray(str).map(createTextNode); + return asHtml + ? chunkStringAsHtml(str, stringSpliterator) + : toArray(stringSpliterator(str)).map(createTextNode); } diff --git a/packages/typeit/src/helpers/expandTextNodes.ts b/packages/typeit/src/helpers/expandTextNodes.ts index fab067cf..52dfa37f 100644 --- a/packages/typeit/src/helpers/expandTextNodes.ts +++ b/packages/typeit/src/helpers/expandTextNodes.ts @@ -1,10 +1,13 @@ import createTextNode from "./createTextNode"; import { El } from "../types"; -let expandTextNodes = (element: El): El => { +const expandTextNodes = ( + element: El, + stringSpliterator: (str: string) => string[] +): El => { [...element.childNodes].forEach((child) => { if (child.nodeValue) { - [...child.nodeValue].forEach((c) => { + stringSpliterator(child.nodeValue).forEach((c) => { child.parentNode.insertBefore(createTextNode(c), child); }); @@ -12,7 +15,7 @@ let expandTextNodes = (element: El): El => { return; } - expandTextNodes(child as El); + expandTextNodes(child as El, stringSpliterator); }); return element; diff --git a/packages/typeit/src/helpers/getAllChars.ts b/packages/typeit/src/helpers/getAllChars.ts index 08c6942a..6f7f4980 100644 --- a/packages/typeit/src/helpers/getAllChars.ts +++ b/packages/typeit/src/helpers/getAllChars.ts @@ -7,9 +7,16 @@ import { walkElementNodes } from "./chunkStrings"; * Get a flattened array of text nodes that have been typed. * This excludes any cursor character that might exist. */ -let getAllChars = (element: El) => { +let getAllChars = ( + element: El, + stringSpliterator: (str: string) => string[] +) => { if (isInput(element)) { - return toArray(element.value); + if (typeof element.value === "string") { + return toArray(stringSpliterator(element.value)); + } else { + return toArray(element.value); + } } return walkElementNodes(element, true).filter( diff --git a/packages/typeit/src/helpers/getParsedBody.ts b/packages/typeit/src/helpers/getParsedBody.ts index defb0348..96833c22 100644 --- a/packages/typeit/src/helpers/getParsedBody.ts +++ b/packages/typeit/src/helpers/getParsedBody.ts @@ -5,9 +5,9 @@ import expandTextNodes from "./expandTextNodes"; * Parse a string as HTML and return the body * of the parsed document, with all text nodes expanded. */ -export default (content): El => { +export default (content, stringSpliterator: (str: string) => string[]): El => { let doc = document.implementation.createHTMLDocument(); doc.body.innerHTML = content; - return expandTextNodes(doc.body as El); + return expandTextNodes(doc.body as El, stringSpliterator); }; diff --git a/packages/typeit/src/index.ts b/packages/typeit/src/index.ts index f05cc81e..f60bee9b 100644 --- a/packages/typeit/src/index.ts +++ b/packages/typeit/src/index.ts @@ -88,7 +88,7 @@ const TypeIt: TypeItInstance = function (element, options = {}) { let _getPace = (index: number = 0): number => calculatePace(_opts)[index]; - let _getAllChars = (): El[] => getAllChars(_element); + let _getAllChars = (): El[] => getAllChars(_element, _opts.stringSpliterator); let _maybeAppendPause = (opts: ActionOpts = {}) => { let delay = opts.delay; @@ -145,7 +145,10 @@ const TypeIt: TypeItInstance = function (element, options = {}) { return cursor as El; } - cursor.innerHTML = getParsedBody(_opts.cursorChar).innerHTML; + cursor.innerHTML = getParsedBody( + _opts.cursorChar, + _opts.stringSpliterator + ).innerHTML; return cursor as El; }; @@ -253,7 +256,7 @@ const TypeIt: TypeItInstance = function (element, options = {}) { if (_opts.startDelete) { _element.innerHTML = existingMarkup; - expandTextNodes(_element); + expandTextNodes(_element, _opts.stringSpliterator); _addSplitPause( duplicate( @@ -537,7 +540,11 @@ const TypeIt: TypeItInstance = function (element, options = {}) { let { instant } = actionOpts; let bookEndQueueItems = _generateTemporaryOptionQueueItems(actionOpts); - let chars = maybeChunkStringAsHtml(string, _opts.html); + let chars = maybeChunkStringAsHtml( + string, + _opts.html, + _opts.stringSpliterator + ); let charsAsQueueItems = chars.map((char): QueueItem => { return { diff --git a/packages/typeit/src/types.ts b/packages/typeit/src/types.ts index 8e12e225..b0ca7909 100644 --- a/packages/typeit/src/types.ts +++ b/packages/typeit/src/types.ts @@ -33,6 +33,22 @@ export interface Options { startDelay?: number; startDelete?: boolean; strings?: string[] | string; + /** + * String splitter function, can be used to split emoji's or graphemes. + * + * @example + * ```js + * import GraphemeSplitter from "grapheme-splitter"; + * const splitter = new GraphemeSplitter(); + * new TypeIt("#element", { + * strings: "👋🏻👋🏼👋🏽👋🏾👋🏿", + * stringSpliterator: (str) => splitter.splitGraphemes(str), + * }); + * ``` + * @see https://www.npmjs.com/package/grapheme-splitter + * @default null + */ + stringSpliterator?: (str: string) => string[]; waitUntilVisible?: boolean; beforeString?: Function; afterString?: Function;