diff --git a/.eslintignore b/.eslintignore index 21f5091..5fc0180 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,6 +2,10 @@ umd/* dist/* es/* node_modules/* +storybook-static/* private/* examples/* .vscode/* +__stories__/* +__tests__/* + diff --git a/.eslintrc.json b/.eslintrc.json index 7a60bc6..2521b18 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,6 +4,7 @@ "parserOptions": { "ecmaVersion": 12, "sourceType": "module" }, "rules": { "no-lonely-if": 0, + "no-console": 0, "no-plusplus": 0, "no-param-reassign": 0, "no-else-return": 0, diff --git a/__stories__/assets/stories.css b/__stories__/assets/stories.css index e925e43..524fd84 100644 --- a/__stories__/assets/stories.css +++ b/__stories__/assets/stories.css @@ -13,11 +13,6 @@ body { color: var(--color-default-fg); } -.container { - margin: 16px; - padding: 16px; -} - *, *::before, *::after { @@ -25,7 +20,7 @@ body { } .container { - margin: 16px; + margin: 0; padding: 16px; width: 100%; max-width: 732px; diff --git a/__stories__/components/Example.svelte b/__stories__/components/Example.svelte index 66a7338..c996cd3 100644 --- a/__stories__/components/Example.svelte +++ b/__stories__/components/Example.svelte @@ -56,10 +56,11 @@ // Split the the target element(s) using the provided options instance = SplitType.create('.target', options) resizeObserver.observe(containerElement) - console.log(instance) }) onDestroy(() => { + // Cleanup splitType instance + instance.revert() resizeObserver.disconnect() }) diff --git a/__tests__/utils/Data.test.js b/__tests__/utils/Data.test.js index 9a1aa8a..fb93f07 100644 --- a/__tests__/utils/Data.test.js +++ b/__tests__/utils/Data.test.js @@ -1,7 +1,54 @@ -import { Data, removeData } from '../../lib/Data' +import * as data from '../../lib/Data' + +const owner = document.createElement('div') + +afterEach(() => { + data.remove(owner) +}) describe(`Data(owner)`, () => { - it(`Returns empty object when owner is undefined`, () => { - expect(Data(undefined)).toEqual({}) + it(`data.get(undefined) returns empty object`, () => { + expect(data.get(undefined)).toEqual({}) + }) + it(`data.set(owner, key, value) works as expected`, () => { + // Make sure data store is empty at start of test + expect(data.cache).toEqual({}) + data.set(owner, 'foo', 'bar') + expect(data.cache[owner[data.expando]]).toEqual({ foo: 'bar' }) + data.set(owner, 'hello', 'world') + expect(data.cache[owner[data.expando]]).toEqual({ + foo: 'bar', + hello: 'world', + }) + }) + it(`data.set(owner, objectMap) works as expected`, () => { + // Make sure data store is empty at start of test + expect(data.cache).toEqual({}) + data.set(owner, { + foo: 'bar', + hello: 'world', + }) + expect(data.cache[owner[data.expando]]).toEqual({ + foo: 'bar', + hello: 'world', + }) + }) + + it(`data.get(owner) returns data object `, () => { + // Make sure data store is empty at start of test + expect(data.get(owner)).toEqual({}) + // Set multiple properties by passing an object as the second arg + data.set(owner, { + foo: 'bar', + hello: 'world', + }) + // Set an additional property using the (owner, key, value) syntax + data.set(owner, 'other', 'value') + // data.get(owner) should return an object with the the expected props + expect(data.get(owner)).toEqual({ + foo: 'bar', + hello: 'world', + other: 'value', + }) }) }) diff --git a/lib/Data.js b/lib/Data.js index 8ae954b..0e58323 100644 --- a/lib/Data.js +++ b/lib/Data.js @@ -1,4 +1,9 @@ import isObject from './utils/isObject' +import { entries } from './utils/object' + +export const expando = `_splittype` +export const cache = {} +let uid = 0 /** * Stores data associated with DOM elements or other objects. This is a @@ -23,37 +28,52 @@ import isObject from './utils/isObject' * @param {string} key * @param {any} value */ -export function Data(owner, key, value) { - let data = {} - let id = null - - if (isObject(owner)) { - id = owner[Data.expando] || (owner[Data.expando] = ++Data.uid) - data = Data.cache[id] || (Data.cache[id] = {}) +export function set(owner, key, value) { + if (!isObject(owner)) { + console.warn('[data.set] owner is not an object') + return null } - // Get data + const id = owner[expando] || (owner[expando] = ++uid) + const data = cache[id] || (cache[id] = {}) + if (value === undefined) { - if (key === undefined) { - return data + if (!!key && Object.getPrototypeOf(key) === Object.prototype) { + cache[id] = { ...data, ...key } } - return data[key] - } - // Set data - else if (key !== undefined) { + } else if (key !== undefined) { data[key] = value - return value } + return value } -Data.expando = `splitType${new Date() * 1}` -Data.cache = {} -Data.uid = 0 +export function get(owner, key) { + const id = isObject(owner) ? owner[expando] : null + const data = (id && cache[id]) || {} + if (key === undefined) { + return data + } + return data[key] +} -// Remove all data associated with the given element -export function RemoveData(element) { - const id = element && element[Data.expando] +/** + * Remove all data associated with the given element + */ +export function remove(element) { + const id = element && element[expando] if (id) { delete element[id] - delete Data.cache[id] + delete cache[id] } } + +/** + * Remove all temporary data from the store. + */ +export function cleanup() { + entries(cache).forEach(([id, { isRoot, isSplit }]) => { + if (!isRoot || !isSplit) { + cache[id] = null + delete cache[id] + } + }) +} diff --git a/lib/SplitType.js b/lib/SplitType.js index 0c69c26..36e9615 100644 --- a/lib/SplitType.js +++ b/lib/SplitType.js @@ -1,9 +1,8 @@ import extend from './utils/extend' import parseSettings from './utils/parseSettings' import parseTypes from './utils/parseTypes' -import toArray from './utils/toArray' import getTargetElements from './utils/getTargetElements' -import { Data, RemoveData } from './Data' +import * as data from './Data' import split from './split' import repositionAfterSplit from './repositionAfterSplit' import defaults from './defaults' @@ -12,8 +11,16 @@ import defaults from './defaults' let _defaults = extend(defaults, {}) export default class SplitType { + /** + * The internal data store + */ + static get data() { + return data.cache + } + /** * The default settings for all splitType instances + * @static */ static get defaults() { return _defaults @@ -27,6 +34,7 @@ export default class SplitType { * * @param {Object} settings an object containing the settings to override * @deprecated + * @static * @example * SplitType.defaults = { "position": "absolute" } */ @@ -40,6 +48,8 @@ export default class SplitType { * * @param {Object} settings an object containing the settings to override * @returns {Object} the new default settings + * @public + * @static * @example * SplitType.setDefaults({ "position": "absolute" }) */ @@ -49,34 +59,43 @@ export default class SplitType { } /** - * A static method to revert the target elements. - * This provides a way to revert elements without having a reference to the - * SplitType instance. + * Revert target elements to their original html content + * Has no effect on that + * + * @param {any} elements The target elements to revert. One of: + * - {string} A css selector + * - {HTMLElement} A single element + * - {NodeList} A NodeList or collection + * - {HTMLElement[]} An array of Elements + * - {Array} A nested array of elements + * @static */ - static revert(target) { - const elements = getTargetElements(target) - elements.forEach((element) => { - const { isSplit, html } = Data(element) + static revert(elements) { + getTargetElements(elements).forEach((element) => { + const { isSplit, html, cssWidth, cssHeight } = data.get(element) if (isSplit) { - element.innerHTML = html || '' - Data(element).isSplit = false - Data(element).html = null + element.innerHTML = html + element.style.width = cssWidth || '' + element.style.height = cssHeight || '' + data.remove(element) } }) } /** - * Creates a new SplitType instance with the given parameters - * This static method provides a way to create a splitType instance without - * using the new keyword. + * Creates a new SplitType instance + * This static method provides a way to create a `SplitType` instance without + * using the `new` keyword. * - * @param {any} target The target elements to split. can be one of: + * @param {any} target The target elements to split. One of: * - {string} A css selector * - {HTMLElement} A single element - * - {ArrayLike} A collection of elements - * - {Array>} A nested array of elements + * - {NodeList} A NodeList or collection + * - {HTMLElement[]} An array of Elements + * - {Array} A nested array of elements * @param {Object} [options] Settings for the SplitType instance * @return {SplitType} the SplitType instance + * @static */ static create(target, options) { return new SplitType(target, options) @@ -85,24 +104,27 @@ export default class SplitType { /** * Creates a new `SplitType` instance * - * @param {any} target The target elements to split. can be one of: + * @param {any} elements The target elements to split. One of: * - {string} A css selector * - {HTMLElement} A single element - * - {ArrayLike} A collection of elements - * - {Array>} A nested array of elements + * - {NodeList} A NodeList or collection + * - {HTMLElement[]} An array of Elements + * - {Array} A nested array of elements * @param {Object} [options] Settings for the SplitType instance */ - constructor(target, options) { + constructor(elements, options) { this.isSplit = false this.settings = extend(_defaults, parseSettings(options)) - this.elements = getTargetElements(target) || [] - + this.elements = getTargetElements(elements) + // Revert target elements (if they are already split) + // Note: we need to call `revert` in the constructor before caching the + // original html content of the target elements. this.revert() - + // Store the original html content of each target element this.elements.forEach((element) => { - // Store original html content of each target element - Data(element).html = element.innerHTML + data.set(element, 'html', element.innerHTML) }) + // Start the split process this.split() } @@ -114,7 +136,10 @@ export default class SplitType { * @public */ split(options) { - // Revert elements if they are already split + // Revert target elements (if they are already split) + // Note: revert was already called once in the constructor. However, we + // need to call it again here so text is reverted when the user manually + // calls the `split` method to re-split text. this.revert() // Create arrays to hold the split lines, words, and characters @@ -141,8 +166,7 @@ export default class SplitType { this.elements.forEach((element) => { // Add the split text nodes from this element to the arrays of all split // text nodes for this instance. - Data(element).isRoot = true - // const copy = element.cloneNode(true) + data.set(element, 'isRoot', true) const { words, chars } = split(element, this.settings) this.words = [...this.words, ...words] this.chars = [...this.chars, ...chars] @@ -157,43 +181,29 @@ export default class SplitType { // Set isSplit to true for the SplitType instance this.isSplit = true - // Set scroll position to cached value. window.scrollTo(scrollPos[0], scrollPos[1]) - - // Clear data cache - this.elements.forEach((element) => { - // Deletes cached data for nodes within the target element. - // Does not remove data stored on the target element itself. - toArray(Data(element).nodes).forEach(RemoveData) - Data(element).nodes = null - }) + // Clean up stored data + data.cleanup() } /** * Reverts target element(s) back to their original html content + * Deletes all stored data associated with the target elements + * Resets the properties on the splitType instance + * * @public */ revert() { - // restore original HTML content - // Note: this will revert the target elements even if they were split by a - // different splitType instance. - this.elements.forEach((element) => { - const { isSplit, html, cssWidth, cssHeight } = Data(element) - if (isSplit) { - element.innerHTML = html - element.style.width = cssWidth || '' - element.style.height = cssHeight || '' - Data(element).isSplit = false - } - }) - - // Reset instance properties if necessary if (this.isSplit) { + // Reset instance properties if necessary this.lines = null this.words = null this.chars = null this.isSplit = false } + SplitType.revert(this.elements) } } + +window.SplitType = SplitType diff --git a/lib/repositionAfterSplit.js b/lib/repositionAfterSplit.js index b88899f..8c4db29 100644 --- a/lib/repositionAfterSplit.js +++ b/lib/repositionAfterSplit.js @@ -3,7 +3,7 @@ import parseTypes from './utils/parseTypes' import getPosition from './utils/getPosition' import createElement from './utils/createElement' import unSplitWords from './unSplitWords' -import { Data } from './Data' +import * as data from './Data' const createFragment = () => document.createDocumentFragment() @@ -19,10 +19,6 @@ export default function repositionAfterSplit(element, settings, scrollPos) { let contentBox let lines = [] - // We store the nodeList temporarily so we can clear all stored data for these - // nodes once the split operation is complete. - Data(element).nodes = nodes - /**------------------------------------------------ ** GET STYLES AND POSITIONS **-----------------------------------------------*/ @@ -70,8 +66,10 @@ export default function repositionAfterSplit(element, settings, scrollPos) { elementHeight = element.offsetHeight // Store the original inline height and width of the element - Data(element).cssWidth = element.style.width - Data(element).cssHeight = element.style.height + data.set(element, { + cssWidth: element.style.width, + cssHeight: element.style.height, + }) } // Iterate over every node in the target element @@ -79,6 +77,7 @@ export default function repositionAfterSplit(element, settings, scrollPos) { // node is a word element or custom html element const isWordLike = node.parentElement === element // TODO needs work + // Get te size and position of split text nodes const { width, height, top, left } = getPosition( node, isWordLike, @@ -101,16 +100,9 @@ export default function repositionAfterSplit(element, settings, scrollPos) { wordsInCurrentLine.push(node) } // END IF - // Get the size and position of all split text nodes. if (settings.absolute) { - // The values are stored using the data method - // All split nodes have the same height (lineHeight). So its only - // retrieved once. - // If offset top has already been cached (step 11 a) use the stored value. - Data(node).top = top - Data(node).left = left - Data(node).width = width - Data(node).height = height + // Store the size and position split text nodes + data.set(node, { top, left, width, height }) } }) // END LOOP @@ -133,7 +125,7 @@ export default function repositionAfterSplit(element, settings, scrollPos) { class: `${settings.splitClass} ${settings.lineClass}`, style: `display: block; text-align: ${align}; width: 100%;`, }) - Data(lineElement).isLine = true + data.set(lineElement, 'isLine', true) const lineDimensions = { height: 0, top: 1e4 } @@ -143,7 +135,7 @@ export default function repositionAfterSplit(element, settings, scrollPos) { // Iterate over the word-level elements in the current line. // Note: wordOrElement can either be a word node or nested element wordsInThisLine.forEach((wordOrElement, idx, arr) => { - const { isWordEnd, top, height } = Data(wordOrElement) + const { isWordEnd, top, height } = data.get(wordOrElement) const next = arr[idx + 1] // Determine line height / y-position @@ -159,14 +151,16 @@ export default function repositionAfterSplit(element, settings, scrollPos) { // Determine if there should space after the current element... // If this is not the last word on the current line. // TODO - logic for handing spacing can be improved - if (isWordEnd && Data(next).isWordStart) { + if (isWordEnd && data.get(next).isWordStart) { lineElement.append(' ') } }) // END LOOP if (settings.absolute) { - Data(lineElement).height = lineDimensions.height - Data(lineElement).top = lineDimensions.top + data.set(lineElement, { + height: lineDimensions.height, + top: lineDimensions.top, + }) } return lineElement @@ -196,14 +190,14 @@ export default function repositionAfterSplit(element, settings, scrollPos) { // Iterate over all child elements toArray(nodes).forEach((node) => { - const { isLine, top, left, width, height } = Data(node) - const parentNode = Data(node.parentElement) - const isChildOfLineNode = !isLine && parentNode.isLine + const { isLine, top, left, width, height } = data.get(node) + const parentData = data.get(node.parentElement) + const isChildOfLineNode = !isLine && parentData.isLine // Set the top position of the current node. // -> If `node` a line element, we use the top offset of its first child // -> If `node` the child of line element, then its top offset is zero - node.style.top = `${isChildOfLineNode ? top - parentNode.top : top}px` + node.style.top = `${isChildOfLineNode ? top - parentData.top : top}px` // Set the left position of the current node. // -> IF `node` is a line element, this is equal to the position left of diff --git a/lib/split.js b/lib/split.js index 8994655..db140e0 100644 --- a/lib/split.js +++ b/lib/split.js @@ -1,6 +1,6 @@ import toArray from './utils/toArray' import splitWordsAndChars from './splitWordsAndChars' -import { Data } from './Data' +import * as data from './Data' /** * Splits the text content of a target element into words and/or characters. @@ -33,9 +33,9 @@ export default function split(node, settings) { const childNodes = toArray(node.childNodes) if (childNodes.length) { - Data(node).isSplit = true + data.set(node, 'isSplit', true) // we need to set a few styles on nested html elements - if (!Data(node).isRoot) { + if (!data.get(node).isRoot) { node.style.display = 'inline-block' node.style.position = 'relative' // To maintain original spacing around nested elements when we are @@ -49,8 +49,10 @@ export default function split(node, settings) { const text = node.textContent || '' const textAfter = nextSibling ? nextSibling.textContent : ' ' const textBefore = prevSibling ? prevSibling.textContent : ' ' - Data(node).isWordEnd = /\s$/.test(text) || /^\s/.test(textAfter) - Data(node).isWordStart = /^\s/.test(text) || /\s$/.test(textBefore) + data.set(node, { + isWordEnd: /\s$/.test(text) || /^\s/.test(textAfter), + isWordStart: /^\s/.test(text) || /\s$/.test(textBefore), + }) } } diff --git a/lib/splitWordsAndChars.js b/lib/splitWordsAndChars.js index 794f3ef..dfb8721 100644 --- a/lib/splitWordsAndChars.js +++ b/lib/splitWordsAndChars.js @@ -4,7 +4,7 @@ import createElement from './utils/createElement' import parseTypes from './utils/parseTypes' import extend from './utils/extend' import defaults from './defaults' -import { Data } from './Data' +import * as data from './Data' /** * Splits the text content of a single TextNode into words and/or characters. @@ -51,7 +51,7 @@ export default function splitWordsAndChars(textNode, settings) { style: 'display: inline-block;', children: CHAR, }) - Data(characterElement).isChar = true + data.set(characterElement, 'isChar', true) chars = [...chars, characterElement] return characterElement }) @@ -70,9 +70,11 @@ export default function splitWordsAndChars(textNode, settings) { }`, children: types.chars ? characterElementsForCurrentWord : WORD, }) - Data(wordElement).isWord = true - Data(wordElement).isWordStart = true - Data(wordElement).isWordEnd = true + data.set(wordElement, { + isWord: true, + isWordStart: true, + isWordEnd: true, + }) splitText.appendChild(wordElement) } else { // -> If NOT splitting into words OR lines... diff --git a/lib/unSplitWords.js b/lib/unSplitWords.js index 9c580db..0cecbe7 100644 --- a/lib/unSplitWords.js +++ b/lib/unSplitWords.js @@ -1,5 +1,5 @@ import toArray from './utils/toArray' -import { Data } from './Data' +import * as data from './Data' /** * Recursively "un-splits" text into words. @@ -10,9 +10,10 @@ import { Data } from './Data' * @return {void} */ export default function unSplitWords(element) { - if (!Data(element).isWord) { + if (!data.get(element).isWord) { toArray(element.children).forEach((child) => unSplitWords(child)) } else { + data.remove(element) element.replaceWith(...element.childNodes) } } diff --git a/lib/utils/object.js b/lib/utils/object.js new file mode 100644 index 0000000..7ed6706 --- /dev/null +++ b/lib/utils/object.js @@ -0,0 +1 @@ +export const { entries, keys, values } = Object