UNPKG

splitting

Version:

Micro-library to split a DOM element's words & chars into elements populated with CSS vars.

384 lines (338 loc) 9.86 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.Splitting = factory()); }(this, (function () { 'use strict'; var root = document; var createText = root.createTextNode.bind(root); /** * # setProperty * Apply a CSS var * @param {HTMLElement} el * @param {string} varName * @param {string|number} value */ function setProperty(el, varName, value) { el.style.setProperty(varName, value); } /** * * @param {!HTMLElement} el * @param {!HTMLElement} child */ function appendChild(el, child) { return el.appendChild(child); } /** * * @param {!HTMLElement} parent * @param {string} key * @param {string} text * @param {boolean} whitespace */ function createElement(parent, key, text, whitespace) { var el = root.createElement('span'); key && (el.className = key); if (text) { !whitespace && el.setAttribute("data-" + key, text); el.textContent = text; } return (parent && appendChild(parent, el)) || el; } /** * * @param {!HTMLElement} el * @param {string} key */ function getData(el, key) { return el.getAttribute("data-" + key) } /** * * @param {import('../types').Target} e * @param {!HTMLElement} parent * @returns {!Array<!HTMLElement>} */ function $(e, parent) { return !e || e.length == 0 ? // null or empty string returns empty array [] : e.nodeName ? // a single element is wrapped in an array [e] : // selector and NodeList are converted to Element[] [].slice.call(e[0].nodeName ? e : (parent || root).querySelectorAll(e)); } /** * Creates and fills an array with the value provided * @param {number} len * @param {() => T} valueProvider * @return {T} * @template T */ /** * A for loop wrapper used to reduce js minified size. * @param {!Array<T>} items * @param {function(T):void} consumer * @template T */ function each(items, consumer) { items && items.some(consumer); } /** * @param {T} obj * @return {function(string):*} * @template T */ function selectFrom(obj) { return function (key) { return obj[key]; } } /** * # Splitting.index * Index split elements and add them to a Splitting instance. * * @param {HTMLElement} element * @param {string} key * @param {!Array<!HTMLElement> | !Array<!Array<!HTMLElement>>} items */ function index(element, key, items) { var prefix = '--' + key; var cssVar = prefix + "-index"; each(items, function (items, i) { if (Array.isArray(items)) { each(items, function(item) { setProperty(item, cssVar, i); }); } else { setProperty(items, cssVar, i); } }); setProperty(element, prefix + "-total", items.length); } /** * @type {Record<string, import('./types').ISplittingPlugin>} */ var plugins = {}; /** * @param {string} by * @param {string} parent * @param {!Array<string>} deps * @return {!Array<string>} */ function resolvePlugins(by, parent, deps) { // skip if already visited this dependency var index = deps.indexOf(by); if (index == -1) { // if new to dependency array, add to the beginning deps.unshift(by); // recursively call this function for all dependencies var plugin = plugins[by]; if (!plugin) { throw new Error("plugin not loaded: " + by); } each(plugin.depends, function(p) { resolvePlugins(p, by, deps); }); } else { // if this dependency was added already move to the left of // the parent dependency so it gets loaded in order var indexOfParent = deps.indexOf(parent); deps.splice(index, 1); deps.splice(indexOfParent, 0, by); } return deps; } /** * Internal utility for creating plugins... essentially to reduce * the size of the library * @param {string} by * @param {string} key * @param {string[]} depends * @param {Function} split * @returns {import('./types').ISplittingPlugin} */ function createPlugin(by, depends, key, split) { return { by: by, depends: depends, key: key, split: split } } /** * * @param {string} by * @returns {import('./types').ISplittingPlugin[]} */ function resolve(by) { return resolvePlugins(by, 0, []).map(selectFrom(plugins)); } /** * Adds a new plugin to splitting * @param {import('./types').ISplittingPlugin} opts */ function add(opts) { plugins[opts.by] = opts; } /** * # Splitting.split * Split an element's textContent into individual elements * @param {!HTMLElement} el Element to split * @param {string} key * @param {string} splitOn * @param {boolean} includePrevious * @param {boolean} preserveWhitespace * @return {!Array<!HTMLElement>} */ function splitText(el, key, splitOn, includePrevious, preserveWhitespace) { // Combine any strange text nodes or empty whitespace. el.normalize(); // Use fragment to prevent unnecessary DOM thrashing. var elements = []; var F = document.createDocumentFragment(); if (includePrevious) { elements.push(el.previousSibling); } var allElements = []; $(el.childNodes).some(function(next) { if (next.tagName && !next.hasChildNodes()) { // keep elements without child nodes (no text and no children) allElements.push(next); return; } // Recursively run through child nodes if (next.childNodes && next.childNodes.length) { allElements.push(next); elements.push.apply(elements, splitText(next, key, splitOn, includePrevious, preserveWhitespace)); return; } // Get the text to split, trimming out the whitespace /** @type {string} */ var wholeText = next.wholeText || ''; var contents = wholeText.trim(); // If there's no text left after trimming whitespace, continue the loop if (contents.length) { // insert leading space if there was one if (wholeText[0] === ' ') { allElements.push(createText(' ')); } // Concatenate the split text children back into the full array var useSegmenter = splitOn === "" && typeof Intl.Segmenter === "function"; each(useSegmenter ? Array.from(new Intl.Segmenter().segment(contents)).map(function(x){return x.segment}) : contents.split(splitOn), function (splitText, i) { if (i && preserveWhitespace) { allElements.push(createElement(F, "whitespace", " ", preserveWhitespace)); } var splitEl = createElement(F, key, splitText); elements.push(splitEl); allElements.push(splitEl); }); // insert trailing space if there was one if (wholeText[wholeText.length - 1] === ' ') { allElements.push(createText(' ')); } } }); each(allElements, function(el) { appendChild(F, el); }); // Clear out the existing element el.innerHTML = ""; appendChild(el, F); return elements; } /** an empty value */ var _ = 0; function copy(dest, src) { for (var k in src) { dest[k] = src[k]; } return dest; } var WORDS = 'words'; var wordPlugin = createPlugin( /* by= */ WORDS, /* depends= */ _, /* key= */ 'word', /* split= */ function(el) { return splitText(el, 'word', /\s+/, 0, 1) } ); var CHARS = "chars"; var charPlugin = createPlugin( /* by= */ CHARS, /* depends= */ [WORDS], /* key= */ "char", /* split= */ function(el, options, ctx) { var results = []; each(ctx[WORDS], function(word, i) { results.push.apply(results, splitText(word, "char", "", options.whitespace && i)); }); return results; } ); /** * # Splitting * * @param {import('./types').ISplittingOptions} opts * @return {!Array<*>} */ function Splitting (opts) { opts = opts || {}; var key = opts.key; return $(opts.target || '[data-splitting]').map(function(el) { var ctx = el['🍌']; if (!opts.force && ctx) { return ctx; } ctx = el['🍌'] = { el: el }; var by = opts.by || getData(el, 'splitting'); if (!by || by == 'true') { by = CHARS; } var items = resolve(by); var opts2 = copy({}, opts); each(items, function(plugin) { if (plugin.split) { var pluginBy = plugin.by; var key2 = (key ? '-' + key : '') + plugin.key; var results = plugin.split(el, opts2, ctx); key2 && index(el, key2, results); ctx[pluginBy] = results; el.classList.add(pluginBy); } }); el.classList.add('splitting'); return ctx; }) } /** * # Splitting.html * * @param {import('./types').ISplittingOptions} opts */ function html(opts) { opts = opts || {}; var parent = opts.target = createElement(); parent.innerHTML = opts.content; Splitting(opts); return parent.outerHTML } Splitting.html = html; Splitting.add = add; // import { linePlugin } from "./plugins/lines"; // import { itemPlugin } from "./plugins/items"; // import { rowPlugin } from "./plugins/rows"; // import { columnPlugin } from "./plugins/columns"; // import { gridPlugin } from "./plugins/grid"; // import { layoutPlugin } from "./plugins/layout"; // import { cellRowPlugin } from "./plugins/cellRows"; // import { cellColumnPlugin } from "./plugins/cellColumns"; // import { cellPlugin } from "./plugins/cells"; // install plugins // word/char plugins add(wordPlugin); add(charPlugin); return Splitting; })));