(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} */ 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 */ function Array2D(len) { var a = []; for (; len--; ) { a[len] = []; } return a; } /** * A for loop wrapper used to reduce js minified size. * @param {!Array} 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 | !Array>} 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} */ var plugins = {}; /** * @param {string} by * @param {string} parent * @param {!Array} deps * @return {!Array} */ 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} */ 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 each(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; /** * Detects the grid by measuring which elements align to a side of it. * @param {!HTMLElement} el * @param {import('../core/types').ISplittingOptions} options * @param {*} side */ function detectGrid(el, options, side) { var items = $(options.matching || el.children, el); var c = {}; each(items, function(w) { var val = Math.round(w[side]); (c[val] || (c[val] = [])).push(w); }); return Object.keys(c).map(Number).sort(byNumber).map(selectFrom(c)); } /** * Sorting function for numbers. * @param {number} a * @param {number} b * @return {number} */ function byNumber(a, b) { return a - b; } var linePlugin = createPlugin( /* by= */ 'lines', /* depends= */ [WORDS], /* key= */ 'line', /* split= */ function(el, options, ctx) { return detectGrid(el, { matching: ctx[WORDS] }, 'offsetTop') } ); var itemPlugin = createPlugin( /* by= */ 'items', /* depends= */ _, /* key= */ 'item', /* split= */ function(el, options) { return $(options.matching || el.children, el) } ); var rowPlugin = createPlugin( /* by= */ 'rows', /* depends= */ _, /* key= */ 'row', /* split= */ function(el, options) { return detectGrid(el, options, "offsetTop"); } ); var columnPlugin = createPlugin( /* by= */ 'cols', /* depends= */ _, /* key= */ "col", /* split= */ function(el, options) { return detectGrid(el, options, "offsetLeft"); } ); var gridPlugin = createPlugin( /* by= */ 'grid', /* depends= */ ['rows', 'cols'] ); var LAYOUT = "layout"; var layoutPlugin = createPlugin( /* by= */ LAYOUT, /* depends= */ _, /* key= */ _, /* split= */ function(el, opts) { // detect and set options var rows = opts.rows = +(opts.rows || getData(el, 'rows') || 1); var columns = opts.columns = +(opts.columns || getData(el, 'columns') || 1); // Seek out the first if the value is true opts.image = opts.image || getData(el, 'image') || el.currentSrc || el.src; if (opts.image) { var img = $("img", el)[0]; opts.image = img && (img.currentSrc || img.src); } // add optional image to background if (opts.image) { setProperty(el, "background-image", "url(" + opts.image + ")"); } var totalCells = rows * columns; var elements = []; var container = createElement(_, "cell-grid"); while (totalCells--) { // Create a span var cell = createElement(container, "cell"); createElement(cell, "cell-inner"); elements.push(cell); } // Append elements back into the parent appendChild(el, container); return elements; } ); var cellRowPlugin = createPlugin( /* by= */ "cellRows", /* depends= */ [LAYOUT], /* key= */ "row", /* split= */ function(el, opts, ctx) { var rowCount = opts.rows; var result = Array2D(rowCount); each(ctx[LAYOUT], function(cell, i, src) { result[Math.floor(i / (src.length / rowCount))].push(cell); }); return result; } ); var cellColumnPlugin = createPlugin( /* by= */ "cellColumns", /* depends= */ [LAYOUT], /* key= */ "col", /* split= */ function(el, opts, ctx) { var columnCount = opts.columns; var result = Array2D(columnCount); each(ctx[LAYOUT], function(cell, i) { result[i % columnCount].push(cell); }); return result; } ); var cellPlugin = createPlugin( /* by= */ "cells", /* depends= */ ['cellRows', 'cellColumns'], /* key= */ "cell", /* split= */ function(el, opt, ctx) { // re-index the layout as the cells return ctx[LAYOUT]; } ); // install plugins // word/char plugins add(wordPlugin); add(charPlugin); add(linePlugin); // grid plugins add(itemPlugin); add(rowPlugin); add(columnPlugin); add(gridPlugin); // cell-layout plugins add(layoutPlugin); add(cellRowPlugin); add(cellColumnPlugin); add(cellPlugin); return Splitting; })));