├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE.md ├── README.md ├── config ├── compress.json ├── rollup.all.compressed.js ├── rollup.all.js ├── rollup.lite.compressed.js └── rollup.lite.js ├── dist ├── splitting-cells.css ├── splitting-lite.js ├── splitting-lite.min.js ├── splitting.css ├── splitting.js └── splitting.min.js ├── package-lock.json ├── package.json ├── src ├── all.js ├── core │ ├── plugin-manager.js │ ├── splitting.js │ └── types.ts ├── lite.js ├── plugins │ ├── cellColumns.js │ ├── cellRows.js │ ├── cells.js │ ├── chars.js │ ├── columns.js │ ├── grid.js │ ├── items.js │ ├── layout.js │ ├── lines.js │ ├── rows.js │ └── words.js └── utils │ ├── arrays.js │ ├── css-vars.js │ ├── detect-grid.js │ ├── dom.js │ ├── objects.js │ └── split-text.js └── tests ├── _setup.js ├── features ├── splitting.cells.js ├── splitting.chars.js ├── splitting.grid.js ├── splitting.html.js ├── splitting.items.js ├── splitting.js ├── splitting.lines.js └── splitting.words.js └── utils └── dom.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { "modules": false }]], 3 | "plugins": ["transform-es2015-modules-commonjs"], 4 | "sourceMap" : "inline" 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/Icon* 2 | node_modules/ 3 | src/original.js 4 | src/Splitting.string.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": [ 12 | "--runInBand" 13 | ], 14 | "cwd": "${workspaceFolder}", 15 | "console": "integratedTerminal", 16 | "internalConsoleOptions": "neverOpen", 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 18 | "sourceMaps": true 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Stephen Shaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Splitting.js](https://splitting.js.org) 2 | 3 | ### _CSS Vars for split words, chars & more!_ 4 | 5 | ![The current build status based on whether tests are passing](https://api.travis-ci.org/shshaw/Splitting.svg?branch=1.0.0) 6 | ![The Uncompressed size of Splitting](https://img.shields.io/bundlephobia/min/splitting.svg?label=Minified%20Size) 7 | ![The GZIP size of Splitting](https://img.shields.io/bundlephobia/minzip/splitting.svg?label=GZIP%20Size) 8 | ![License: MIT](https://img.shields.io/npm/l/splitting.svg?label=License) 9 | 10 | Splitting.js is a JavaScript microlibrary designed to split (section off) an element in a variety of ways, such as words, characters, child nodes, and more! 11 | 12 | Most Splitting methods utilize a series of ``s populated with CSS variables and data attributes unlocking transitions and animations that were previously not feasible with CSS. 13 | 14 | Install with `npm i splitting -s` or [Download](https://github.com/shshaw/Splitting/archive/master.zip). 15 | 16 | Consult the [guide & documentation](https://splitting.js.org/guide.html) for more details and installation instructions. 17 | 18 | - [**Guide & Documentation**](https://splitting.js.org/guide.html) 19 | - [**Demos**](https://codepen.io/collection/43588e4b7beaaf25ede7e38e61441e54/) 20 | 21 | --- 22 | 23 | ## Maintainers 24 | 25 | | Maintainer | GitHub | Twitter | 26 | | :- | :- | :- | 27 | | Stephen Shaw | [@shshaw](https://github.com/shshaw) | [@shshaw](https://twitter.com/shshaw) | 28 | | Christopher Wallis | [@notoriousb1t](https://github.com/notoriousb1t) | [@notoriousb1t](https://twitter.com/notoriousb1t) | 29 | -------------------------------------------------------------------------------- /config/compress.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": "verbose", 3 | "compress": { 4 | "collapse_vars": true, 5 | "comparisons": true, 6 | "conditionals": true, 7 | "dead_code": true, 8 | "drop_console": true, 9 | "evaluate": false, 10 | "if_return": true, 11 | "inline": true, 12 | "reduce_vars": true, 13 | "loops": true, 14 | "passes": 1, 15 | "unsafe_comps": true, 16 | "typeofs": false 17 | }, 18 | "output": { 19 | "semicolons": false 20 | }, 21 | "mangle": true, 22 | "toplevel": false, 23 | "ie8": false 24 | } -------------------------------------------------------------------------------- /config/rollup.all.compressed.js: -------------------------------------------------------------------------------- 1 | import size from 'rollup-plugin-filesize'; 2 | import { uglify } from 'rollup-plugin-uglify'; 3 | import { minify } from 'uglify-js'; 4 | import uglifyOptions from './compress.json'; 5 | 6 | export default { 7 | input: 'src/all.js', 8 | output: [ 9 | { file: 'dist/splitting.min.js', name: 'Splitting', format: 'umd' }, 10 | ], 11 | plugins: [ 12 | size(), 13 | uglify(uglifyOptions, minify) 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /config/rollup.all.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'src/all.js', 3 | output: [ 4 | { file: 'dist/splitting.js', name: 'Splitting', format: 'umd' } 5 | ] 6 | } -------------------------------------------------------------------------------- /config/rollup.lite.compressed.js: -------------------------------------------------------------------------------- 1 | import size from 'rollup-plugin-filesize'; 2 | import { uglify } from 'rollup-plugin-uglify'; 3 | import { minify } from 'uglify-js'; 4 | import uglifyOptions from './compress.json'; 5 | 6 | export default { 7 | input: 'src/lite.js', 8 | output: [ 9 | { file: 'dist/splitting-lite.min.js', name: 'Splitting', format: 'umd' }, 10 | ], 11 | plugins: [ 12 | size(), 13 | uglify(uglifyOptions, minify) 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /config/rollup.lite.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: 'src/lite.js', 3 | output: [ 4 | { file: 'dist/splitting-lite.js', name: 'Splitting', format: 'umd' } 5 | ] 6 | } -------------------------------------------------------------------------------- /dist/splitting-cells.css: -------------------------------------------------------------------------------- 1 | .splitting.cells img { width: 100%; display: block; } 2 | 3 | @supports ( display: grid ) { 4 | .splitting.cells { 5 | position: relative; 6 | overflow: hidden; 7 | background-size: cover; 8 | visibility: hidden; 9 | } 10 | 11 | .splitting .cell-grid { 12 | background: inherit; 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | width: 100%; 17 | height: 100%; 18 | display: grid; 19 | grid-template: repeat( var(--row-total), 1fr ) / repeat( var(--col-total), 1fr ); 20 | } 21 | 22 | .splitting .cell { 23 | background: inherit; 24 | position: relative; 25 | overflow: hidden; 26 | } 27 | 28 | .splitting .cell-inner { 29 | background: inherit; 30 | position: absolute; 31 | visibility: visible; 32 | /* Size to fit the whole container size */ 33 | width: calc(100% * var(--col-total)); 34 | height: calc(100% * var(--row-total)); 35 | /* Position properly */ 36 | left: calc(-100% * var(--col-index)); 37 | top: calc(-100% * var(--row-index)); 38 | } 39 | 40 | /* Helper variables for advanced effects */ 41 | .splitting .cell { 42 | --center-x: calc((var(--col-total) - 1) / 2); 43 | --center-y: calc((var(--row-total) - 1) / 2); 44 | 45 | /* Offset from center, positive & negative */ 46 | --offset-x: calc(var(--col-index) - var(--center-x)); 47 | --offset-y: calc(var(--row-index) - var(--center-y)); 48 | 49 | /* Absolute distance from center, only positive */ 50 | --distance-x: calc( (var(--offset-x) * var(--offset-x)) / var(--center-x) ); 51 | 52 | /* Absolute distance from center, only positive */ 53 | --distance-y: calc( (var(--offset-y) * var(--offset-y)) / var(--center-y) ); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /dist/splitting-lite.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.Splitting = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var root = document; 8 | var createText = root.createTextNode.bind(root); 9 | 10 | /** 11 | * # setProperty 12 | * Apply a CSS var 13 | * @param {HTMLElement} el 14 | * @param {string} varName 15 | * @param {string|number} value 16 | */ 17 | function setProperty(el, varName, value) { 18 | el.style.setProperty(varName, value); 19 | } 20 | 21 | /** 22 | * 23 | * @param {!HTMLElement} el 24 | * @param {!HTMLElement} child 25 | */ 26 | function appendChild(el, child) { 27 | return el.appendChild(child); 28 | } 29 | 30 | /** 31 | * 32 | * @param {!HTMLElement} parent 33 | * @param {string} key 34 | * @param {string} text 35 | * @param {boolean} whitespace 36 | */ 37 | function createElement(parent, key, text, whitespace) { 38 | var el = root.createElement('span'); 39 | key && (el.className = key); 40 | if (text) { 41 | !whitespace && el.setAttribute("data-" + key, text); 42 | el.textContent = text; 43 | } 44 | return (parent && appendChild(parent, el)) || el; 45 | } 46 | 47 | /** 48 | * 49 | * @param {!HTMLElement} el 50 | * @param {string} key 51 | */ 52 | function getData(el, key) { 53 | return el.getAttribute("data-" + key) 54 | } 55 | 56 | /** 57 | * 58 | * @param {import('../types').Target} e 59 | * @param {!HTMLElement} parent 60 | * @returns {!Array} 61 | */ 62 | function $(e, parent) { 63 | return !e || e.length == 0 64 | ? // null or empty string returns empty array 65 | [] 66 | : e.nodeName 67 | ? // a single element is wrapped in an array 68 | [e] 69 | : // selector and NodeList are converted to Element[] 70 | [].slice.call(e[0].nodeName ? e : (parent || root).querySelectorAll(e)); 71 | } 72 | 73 | /** 74 | * Creates and fills an array with the value provided 75 | * @param {number} len 76 | * @param {() => T} valueProvider 77 | * @return {T} 78 | * @template T 79 | */ 80 | 81 | 82 | /** 83 | * A for loop wrapper used to reduce js minified size. 84 | * @param {!Array} items 85 | * @param {function(T):void} consumer 86 | * @template T 87 | */ 88 | function each(items, consumer) { 89 | items && items.some(consumer); 90 | } 91 | 92 | /** 93 | * @param {T} obj 94 | * @return {function(string):*} 95 | * @template T 96 | */ 97 | function selectFrom(obj) { 98 | return function (key) { 99 | return obj[key]; 100 | } 101 | } 102 | 103 | /** 104 | * # Splitting.index 105 | * Index split elements and add them to a Splitting instance. 106 | * 107 | * @param {HTMLElement} element 108 | * @param {string} key 109 | * @param {!Array | !Array>} items 110 | */ 111 | function index(element, key, items) { 112 | var prefix = '--' + key; 113 | var cssVar = prefix + "-index"; 114 | 115 | each(items, function (items, i) { 116 | if (Array.isArray(items)) { 117 | each(items, function(item) { 118 | setProperty(item, cssVar, i); 119 | }); 120 | } else { 121 | setProperty(items, cssVar, i); 122 | } 123 | }); 124 | 125 | setProperty(element, prefix + "-total", items.length); 126 | } 127 | 128 | /** 129 | * @type {Record} 130 | */ 131 | var plugins = {}; 132 | 133 | /** 134 | * @param {string} by 135 | * @param {string} parent 136 | * @param {!Array} deps 137 | * @return {!Array} 138 | */ 139 | function resolvePlugins(by, parent, deps) { 140 | // skip if already visited this dependency 141 | var index = deps.indexOf(by); 142 | if (index == -1) { 143 | // if new to dependency array, add to the beginning 144 | deps.unshift(by); 145 | 146 | // recursively call this function for all dependencies 147 | var plugin = plugins[by]; 148 | if (!plugin) { 149 | throw new Error("plugin not loaded: " + by); 150 | } 151 | each(plugin.depends, function(p) { 152 | resolvePlugins(p, by, deps); 153 | }); 154 | } else { 155 | // if this dependency was added already move to the left of 156 | // the parent dependency so it gets loaded in order 157 | var indexOfParent = deps.indexOf(parent); 158 | deps.splice(index, 1); 159 | deps.splice(indexOfParent, 0, by); 160 | } 161 | return deps; 162 | } 163 | 164 | /** 165 | * Internal utility for creating plugins... essentially to reduce 166 | * the size of the library 167 | * @param {string} by 168 | * @param {string} key 169 | * @param {string[]} depends 170 | * @param {Function} split 171 | * @returns {import('./types').ISplittingPlugin} 172 | */ 173 | function createPlugin(by, depends, key, split) { 174 | return { 175 | by: by, 176 | depends: depends, 177 | key: key, 178 | split: split 179 | } 180 | } 181 | 182 | /** 183 | * 184 | * @param {string} by 185 | * @returns {import('./types').ISplittingPlugin[]} 186 | */ 187 | function resolve(by) { 188 | return resolvePlugins(by, 0, []).map(selectFrom(plugins)); 189 | } 190 | 191 | /** 192 | * Adds a new plugin to splitting 193 | * @param {import('./types').ISplittingPlugin} opts 194 | */ 195 | function add(opts) { 196 | plugins[opts.by] = opts; 197 | } 198 | 199 | /** 200 | * # Splitting.split 201 | * Split an element's textContent into individual elements 202 | * @param {!HTMLElement} el Element to split 203 | * @param {string} key 204 | * @param {string} splitOn 205 | * @param {boolean} includePrevious 206 | * @param {boolean} preserveWhitespace 207 | * @return {!Array} 208 | */ 209 | function splitText(el, key, splitOn, includePrevious, preserveWhitespace) { 210 | // Combine any strange text nodes or empty whitespace. 211 | el.normalize(); 212 | 213 | // Use fragment to prevent unnecessary DOM thrashing. 214 | var elements = []; 215 | var F = document.createDocumentFragment(); 216 | 217 | if (includePrevious) { 218 | elements.push(el.previousSibling); 219 | } 220 | 221 | var allElements = []; 222 | $(el.childNodes).some(function(next) { 223 | if (next.tagName && !next.hasChildNodes()) { 224 | // keep elements without child nodes (no text and no children) 225 | allElements.push(next); 226 | return; 227 | } 228 | // Recursively run through child nodes 229 | if (next.childNodes && next.childNodes.length) { 230 | allElements.push(next); 231 | elements.push.apply(elements, splitText(next, key, splitOn, includePrevious, preserveWhitespace)); 232 | return; 233 | } 234 | 235 | // Get the text to split, trimming out the whitespace 236 | /** @type {string} */ 237 | var wholeText = next.wholeText || ''; 238 | var contents = wholeText.trim(); 239 | 240 | // If there's no text left after trimming whitespace, continue the loop 241 | if (contents.length) { 242 | // insert leading space if there was one 243 | if (wholeText[0] === ' ') { 244 | allElements.push(createText(' ')); 245 | } 246 | // Concatenate the split text children back into the full array 247 | var useSegmenter = splitOn === "" && typeof Intl.Segmenter === "function"; 248 | each(useSegmenter ? Array.from(new Intl.Segmenter().segment(contents)).map(function(x){return x.segment}) : contents.split(splitOn), function (splitText, i) { 249 | if (i && preserveWhitespace) { 250 | allElements.push(createElement(F, "whitespace", " ", preserveWhitespace)); 251 | } 252 | var splitEl = createElement(F, key, splitText); 253 | elements.push(splitEl); 254 | allElements.push(splitEl); 255 | }); 256 | // insert trailing space if there was one 257 | if (wholeText[wholeText.length - 1] === ' ') { 258 | allElements.push(createText(' ')); 259 | } 260 | } 261 | }); 262 | 263 | each(allElements, function(el) { 264 | appendChild(F, el); 265 | }); 266 | 267 | // Clear out the existing element 268 | el.innerHTML = ""; 269 | appendChild(el, F); 270 | return elements; 271 | } 272 | 273 | /** an empty value */ 274 | var _ = 0; 275 | 276 | function copy(dest, src) { 277 | for (var k in src) { 278 | dest[k] = src[k]; 279 | } 280 | return dest; 281 | } 282 | 283 | var WORDS = 'words'; 284 | 285 | var wordPlugin = createPlugin( 286 | /* by= */ WORDS, 287 | /* depends= */ _, 288 | /* key= */ 'word', 289 | /* split= */ function(el) { 290 | return splitText(el, 'word', /\s+/, 0, 1) 291 | } 292 | ); 293 | 294 | var CHARS = "chars"; 295 | 296 | var charPlugin = createPlugin( 297 | /* by= */ CHARS, 298 | /* depends= */ [WORDS], 299 | /* key= */ "char", 300 | /* split= */ function(el, options, ctx) { 301 | var results = []; 302 | 303 | each(ctx[WORDS], function(word, i) { 304 | results.push.apply(results, splitText(word, "char", "", options.whitespace && i)); 305 | }); 306 | 307 | return results; 308 | } 309 | ); 310 | 311 | /** 312 | * # Splitting 313 | * 314 | * @param {import('./types').ISplittingOptions} opts 315 | * @return {!Array<*>} 316 | */ 317 | function Splitting (opts) { 318 | opts = opts || {}; 319 | var key = opts.key; 320 | 321 | return $(opts.target || '[data-splitting]').map(function(el) { 322 | var ctx = el['🍌']; 323 | if (!opts.force && ctx) { 324 | return ctx; 325 | } 326 | 327 | ctx = el['🍌'] = { el: el }; 328 | var by = opts.by || getData(el, 'splitting'); 329 | if (!by || by == 'true') { 330 | by = CHARS; 331 | } 332 | var items = resolve(by); 333 | var opts2 = copy({}, opts); 334 | each(items, function(plugin) { 335 | if (plugin.split) { 336 | var pluginBy = plugin.by; 337 | var key2 = (key ? '-' + key : '') + plugin.key; 338 | var results = plugin.split(el, opts2, ctx); 339 | key2 && index(el, key2, results); 340 | ctx[pluginBy] = results; 341 | el.classList.add(pluginBy); 342 | } 343 | }); 344 | 345 | el.classList.add('splitting'); 346 | return ctx; 347 | }) 348 | } 349 | 350 | /** 351 | * # Splitting.html 352 | * 353 | * @param {import('./types').ISplittingOptions} opts 354 | */ 355 | function html(opts) { 356 | opts = opts || {}; 357 | var parent = opts.target = createElement(); 358 | parent.innerHTML = opts.content; 359 | Splitting(opts); 360 | return parent.outerHTML 361 | } 362 | 363 | Splitting.html = html; 364 | Splitting.add = add; 365 | 366 | // import { linePlugin } from "./plugins/lines"; 367 | // import { itemPlugin } from "./plugins/items"; 368 | // import { rowPlugin } from "./plugins/rows"; 369 | // import { columnPlugin } from "./plugins/columns"; 370 | // import { gridPlugin } from "./plugins/grid"; 371 | // import { layoutPlugin } from "./plugins/layout"; 372 | // import { cellRowPlugin } from "./plugins/cellRows"; 373 | // import { cellColumnPlugin } from "./plugins/cellColumns"; 374 | // import { cellPlugin } from "./plugins/cells"; 375 | 376 | // install plugins 377 | // word/char plugins 378 | add(wordPlugin); 379 | add(charPlugin); 380 | 381 | return Splitting; 382 | 383 | }))); 384 | -------------------------------------------------------------------------------- /dist/splitting-lite.min.js: -------------------------------------------------------------------------------- 1 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.Splitting=n()}(this,function(){"use strict" 2 | var u=document,c=u.createTextNode.bind(u) 3 | function f(t,n,e){t.style.setProperty(n,e)}function l(t,n){return t.appendChild(n)}function d(t,n,e,r){var i=u.createElement("span") 4 | return n&&(i.className=n),e&&(r||i.setAttribute("data-"+n,e),i.textContent=e),t&&l(t,i)||i}function n(t,n){return t&&0!=t.length?t.nodeName?[t]:[].slice.call(t[0].nodeName?t:(n||u).querySelectorAll(t)):[]}function p(t,n){t&&t.some(n)}var o={} 5 | function t(t,n,e,r){return{by:t,depends:n,key:e,split:r}}function r(t){return function n(e,t,r){var i=r.indexOf(e) 6 | if(-1==i){r.unshift(e) 7 | var u=o[e] 8 | if(!u)throw new Error("plugin not loaded: "+e) 9 | p(u.depends,function(t){n(t,e,r)})}else u=r.indexOf(t),r.splice(i,1),r.splice(u,0,e) 10 | return r}(t,0,[]).map((n=o,function(t){return n[t]})) 11 | var n}function e(t){o[t.by]=t}function h(t,e,r,i,u){t.normalize() 12 | var o=[],a=document.createDocumentFragment(),s=(i&&o.push(t.previousSibling),[]) 13 | return n(t.childNodes).some(function(t){var n 14 | t.tagName&&!t.hasChildNodes()?s.push(t):t.childNodes&&t.childNodes.length?(s.push(t),o.push.apply(o,h(t,e,r,i,u))):(n=(t=t.wholeText||"").trim()).length&&(" "===t[0]&&s.push(c(" ")),p(""===r&&"function"==typeof Intl.Segmenter?Array.from((new Intl.Segmenter).segment(n)).map(function(t){return t.segment}):n.split(r),function(t,n){n&&u&&s.push(d(a,"whitespace"," ",u)) 15 | n=d(a,e,t) 16 | o.push(n),s.push(n)})," "===t[t.length-1])&&s.push(c(" "))}),p(s,function(t){l(a,t)}),t.innerHTML="",l(t,a),o}var i="words",a=t(i,0,"word",function(t){return h(t,"word",/\s+/,0,1)}),m="chars",s=t(m,[i],"char",function(t,e,n){var r=[] 17 | return p(n[i],function(t,n){r.push.apply(r,h(t,"char","",e.whitespace&&n))}),r}) 18 | function g(e){var c=(e=e||{}).key 19 | return n(e.target||"[data-splitting]").map(function(o){var t,n,a,s=o["🍌"] 20 | return!e.force&&s||(s=o["🍌"]={el:o},n=r(t=(t=e.by||(t="splitting",o.getAttribute("data-"+t)))&&"true"!=t?t:m),a=function(t,n){for(var e in n)t[e]=n[e] 21 | return t}({},e),p(n,function(t){var n,e,r,i,u 22 | t.split&&(n=t.by,r=(c?"-"+c:"")+t.key,t=t.split(o,a,s),r&&(e=o,u=(r="--"+(r=r))+"-index",p(i=t,function(t,n){Array.isArray(t)?p(t,function(t){f(t,u,n)}):f(t,u,n)}),f(e,r+"-total",i.length)),s[n]=t,o.classList.add(n))}),o.classList.add("splitting")),s})}return g.html=function(t){var n=(t=t||{}).target=d() 23 | return n.innerHTML=t.content,g(t),n.outerHTML},(g.add=e)(a),e(s),g}) 24 | -------------------------------------------------------------------------------- /dist/splitting.css: -------------------------------------------------------------------------------- 1 | /* Recommended styles for Splitting */ 2 | .splitting .word, 3 | .splitting .char { 4 | display: inline-block; 5 | } 6 | 7 | /* Psuedo-element chars */ 8 | .splitting .char { 9 | position: relative; 10 | } 11 | 12 | /** 13 | * Populate the psuedo elements with the character to allow for expanded effects 14 | * Set to `display: none` by default; just add `display: block` when you want 15 | * to use the psuedo elements 16 | */ 17 | .splitting .char::before, 18 | .splitting .char::after { 19 | content: attr(data-char); 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | visibility: hidden; 24 | transition: inherit; 25 | user-select: none; 26 | } 27 | 28 | /* Expanded CSS Variables */ 29 | 30 | .splitting { 31 | /* The center word index */ 32 | --word-center: calc((var(--word-total) - 1) / 2); 33 | 34 | /* The center character index */ 35 | --char-center: calc((var(--char-total) - 1) / 2); 36 | 37 | /* The center character index */ 38 | --line-center: calc((var(--line-total) - 1) / 2); 39 | } 40 | 41 | .splitting .word { 42 | /* Pecent (0-1) of the word's position */ 43 | --word-percent: calc(var(--word-index) / var(--word-total)); 44 | 45 | /* Pecent (0-1) of the line's position */ 46 | --line-percent: calc(var(--line-index) / var(--line-total)); 47 | } 48 | 49 | .splitting .char { 50 | /* Percent (0-1) of the char's position */ 51 | --char-percent: calc(var(--char-index) / var(--char-total)); 52 | 53 | /* Offset from center, positive & negative */ 54 | --char-offset: calc(var(--char-index) - var(--char-center)); 55 | 56 | /* Absolute distance from center, only positive */ 57 | --distance: calc( 58 | (var(--char-offset) * var(--char-offset)) / var(--char-center) 59 | ); 60 | 61 | /* Distance from center where -1 is the far left, 0 is center, 1 is far right */ 62 | --distance-sine: calc(var(--char-offset) / var(--char-center)); 63 | 64 | /* Distance from center where 1 is far left/far right, 0 is center */ 65 | --distance-percent: calc((var(--distance) / var(--char-center))); 66 | } 67 | -------------------------------------------------------------------------------- /dist/splitting.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.Splitting = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var root = document; 8 | var createText = root.createTextNode.bind(root); 9 | 10 | /** 11 | * # setProperty 12 | * Apply a CSS var 13 | * @param {HTMLElement} el 14 | * @param {string} varName 15 | * @param {string|number} value 16 | */ 17 | function setProperty(el, varName, value) { 18 | el.style.setProperty(varName, value); 19 | } 20 | 21 | /** 22 | * 23 | * @param {!HTMLElement} el 24 | * @param {!HTMLElement} child 25 | */ 26 | function appendChild(el, child) { 27 | return el.appendChild(child); 28 | } 29 | 30 | /** 31 | * 32 | * @param {!HTMLElement} parent 33 | * @param {string} key 34 | * @param {string} text 35 | * @param {boolean} whitespace 36 | */ 37 | function createElement(parent, key, text, whitespace) { 38 | var el = root.createElement('span'); 39 | key && (el.className = key); 40 | if (text) { 41 | !whitespace && el.setAttribute("data-" + key, text); 42 | el.textContent = text; 43 | } 44 | return (parent && appendChild(parent, el)) || el; 45 | } 46 | 47 | /** 48 | * 49 | * @param {!HTMLElement} el 50 | * @param {string} key 51 | */ 52 | function getData(el, key) { 53 | return el.getAttribute("data-" + key) 54 | } 55 | 56 | /** 57 | * 58 | * @param {import('../types').Target} e 59 | * @param {!HTMLElement} parent 60 | * @returns {!Array} 61 | */ 62 | function $(e, parent) { 63 | return !e || e.length == 0 64 | ? // null or empty string returns empty array 65 | [] 66 | : e.nodeName 67 | ? // a single element is wrapped in an array 68 | [e] 69 | : // selector and NodeList are converted to Element[] 70 | [].slice.call(e[0].nodeName ? e : (parent || root).querySelectorAll(e)); 71 | } 72 | 73 | /** 74 | * Creates and fills an array with the value provided 75 | * @param {number} len 76 | * @param {() => T} valueProvider 77 | * @return {T} 78 | * @template T 79 | */ 80 | function Array2D(len) { 81 | var a = []; 82 | for (; len--; ) { 83 | a[len] = []; 84 | } 85 | return a; 86 | } 87 | 88 | /** 89 | * A for loop wrapper used to reduce js minified size. 90 | * @param {!Array} items 91 | * @param {function(T):void} consumer 92 | * @template T 93 | */ 94 | function each(items, consumer) { 95 | items && items.some(consumer); 96 | } 97 | 98 | /** 99 | * @param {T} obj 100 | * @return {function(string):*} 101 | * @template T 102 | */ 103 | function selectFrom(obj) { 104 | return function (key) { 105 | return obj[key]; 106 | } 107 | } 108 | 109 | /** 110 | * # Splitting.index 111 | * Index split elements and add them to a Splitting instance. 112 | * 113 | * @param {HTMLElement} element 114 | * @param {string} key 115 | * @param {!Array | !Array>} items 116 | */ 117 | function index(element, key, items) { 118 | var prefix = '--' + key; 119 | var cssVar = prefix + "-index"; 120 | 121 | each(items, function (items, i) { 122 | if (Array.isArray(items)) { 123 | each(items, function(item) { 124 | setProperty(item, cssVar, i); 125 | }); 126 | } else { 127 | setProperty(items, cssVar, i); 128 | } 129 | }); 130 | 131 | setProperty(element, prefix + "-total", items.length); 132 | } 133 | 134 | /** 135 | * @type {Record} 136 | */ 137 | var plugins = {}; 138 | 139 | /** 140 | * @param {string} by 141 | * @param {string} parent 142 | * @param {!Array} deps 143 | * @return {!Array} 144 | */ 145 | function resolvePlugins(by, parent, deps) { 146 | // skip if already visited this dependency 147 | var index = deps.indexOf(by); 148 | if (index == -1) { 149 | // if new to dependency array, add to the beginning 150 | deps.unshift(by); 151 | 152 | // recursively call this function for all dependencies 153 | var plugin = plugins[by]; 154 | if (!plugin) { 155 | throw new Error("plugin not loaded: " + by); 156 | } 157 | each(plugin.depends, function(p) { 158 | resolvePlugins(p, by, deps); 159 | }); 160 | } else { 161 | // if this dependency was added already move to the left of 162 | // the parent dependency so it gets loaded in order 163 | var indexOfParent = deps.indexOf(parent); 164 | deps.splice(index, 1); 165 | deps.splice(indexOfParent, 0, by); 166 | } 167 | return deps; 168 | } 169 | 170 | /** 171 | * Internal utility for creating plugins... essentially to reduce 172 | * the size of the library 173 | * @param {string} by 174 | * @param {string} key 175 | * @param {string[]} depends 176 | * @param {Function} split 177 | * @returns {import('./types').ISplittingPlugin} 178 | */ 179 | function createPlugin(by, depends, key, split) { 180 | return { 181 | by: by, 182 | depends: depends, 183 | key: key, 184 | split: split 185 | } 186 | } 187 | 188 | /** 189 | * 190 | * @param {string} by 191 | * @returns {import('./types').ISplittingPlugin[]} 192 | */ 193 | function resolve(by) { 194 | return resolvePlugins(by, 0, []).map(selectFrom(plugins)); 195 | } 196 | 197 | /** 198 | * Adds a new plugin to splitting 199 | * @param {import('./types').ISplittingPlugin} opts 200 | */ 201 | function add(opts) { 202 | plugins[opts.by] = opts; 203 | } 204 | 205 | /** 206 | * # Splitting.split 207 | * Split an element's textContent into individual elements 208 | * @param {!HTMLElement} el Element to split 209 | * @param {string} key 210 | * @param {string} splitOn 211 | * @param {boolean} includePrevious 212 | * @param {boolean} preserveWhitespace 213 | * @return {!Array} 214 | */ 215 | function splitText(el, key, splitOn, includePrevious, preserveWhitespace) { 216 | // Combine any strange text nodes or empty whitespace. 217 | el.normalize(); 218 | 219 | // Use fragment to prevent unnecessary DOM thrashing. 220 | var elements = []; 221 | var F = document.createDocumentFragment(); 222 | 223 | if (includePrevious) { 224 | elements.push(el.previousSibling); 225 | } 226 | 227 | var allElements = []; 228 | $(el.childNodes).some(function(next) { 229 | if (next.tagName && !next.hasChildNodes()) { 230 | // keep elements without child nodes (no text and no children) 231 | allElements.push(next); 232 | return; 233 | } 234 | // Recursively run through child nodes 235 | if (next.childNodes && next.childNodes.length) { 236 | allElements.push(next); 237 | elements.push.apply(elements, splitText(next, key, splitOn, includePrevious, preserveWhitespace)); 238 | return; 239 | } 240 | 241 | // Get the text to split, trimming out the whitespace 242 | /** @type {string} */ 243 | var wholeText = next.wholeText || ''; 244 | var contents = wholeText.trim(); 245 | 246 | // If there's no text left after trimming whitespace, continue the loop 247 | if (contents.length) { 248 | // insert leading space if there was one 249 | if (wholeText[0] === ' ') { 250 | allElements.push(createText(' ')); 251 | } 252 | // Concatenate the split text children back into the full array 253 | var useSegmenter = splitOn === "" && typeof Intl.Segmenter === "function"; 254 | each(useSegmenter ? Array.from(new Intl.Segmenter().segment(contents)).map(function(x){return x.segment}) : contents.split(splitOn), function (splitText, i) { 255 | if (i && preserveWhitespace) { 256 | allElements.push(createElement(F, "whitespace", " ", preserveWhitespace)); 257 | } 258 | var splitEl = createElement(F, key, splitText); 259 | elements.push(splitEl); 260 | allElements.push(splitEl); 261 | }); 262 | // insert trailing space if there was one 263 | if (wholeText[wholeText.length - 1] === ' ') { 264 | allElements.push(createText(' ')); 265 | } 266 | } 267 | }); 268 | 269 | each(allElements, function(el) { 270 | appendChild(F, el); 271 | }); 272 | 273 | // Clear out the existing element 274 | el.innerHTML = ""; 275 | appendChild(el, F); 276 | return elements; 277 | } 278 | 279 | /** an empty value */ 280 | var _ = 0; 281 | 282 | function copy(dest, src) { 283 | for (var k in src) { 284 | dest[k] = src[k]; 285 | } 286 | return dest; 287 | } 288 | 289 | var WORDS = 'words'; 290 | 291 | var wordPlugin = createPlugin( 292 | /* by= */ WORDS, 293 | /* depends= */ _, 294 | /* key= */ 'word', 295 | /* split= */ function(el) { 296 | return splitText(el, 'word', /\s+/, 0, 1) 297 | } 298 | ); 299 | 300 | var CHARS = "chars"; 301 | 302 | var charPlugin = createPlugin( 303 | /* by= */ CHARS, 304 | /* depends= */ [WORDS], 305 | /* key= */ "char", 306 | /* split= */ function(el, options, ctx) { 307 | var results = []; 308 | 309 | each(ctx[WORDS], function(word, i) { 310 | results.push.apply(results, splitText(word, "char", "", options.whitespace && i)); 311 | }); 312 | 313 | return results; 314 | } 315 | ); 316 | 317 | /** 318 | * # Splitting 319 | * 320 | * @param {import('./types').ISplittingOptions} opts 321 | * @return {!Array<*>} 322 | */ 323 | function Splitting (opts) { 324 | opts = opts || {}; 325 | var key = opts.key; 326 | 327 | return $(opts.target || '[data-splitting]').map(function(el) { 328 | var ctx = el['🍌']; 329 | if (!opts.force && ctx) { 330 | return ctx; 331 | } 332 | 333 | ctx = el['🍌'] = { el: el }; 334 | var by = opts.by || getData(el, 'splitting'); 335 | if (!by || by == 'true') { 336 | by = CHARS; 337 | } 338 | var items = resolve(by); 339 | var opts2 = copy({}, opts); 340 | each(items, function(plugin) { 341 | if (plugin.split) { 342 | var pluginBy = plugin.by; 343 | var key2 = (key ? '-' + key : '') + plugin.key; 344 | var results = plugin.split(el, opts2, ctx); 345 | key2 && index(el, key2, results); 346 | ctx[pluginBy] = results; 347 | el.classList.add(pluginBy); 348 | } 349 | }); 350 | 351 | el.classList.add('splitting'); 352 | return ctx; 353 | }) 354 | } 355 | 356 | /** 357 | * # Splitting.html 358 | * 359 | * @param {import('./types').ISplittingOptions} opts 360 | */ 361 | function html(opts) { 362 | opts = opts || {}; 363 | var parent = opts.target = createElement(); 364 | parent.innerHTML = opts.content; 365 | Splitting(opts); 366 | return parent.outerHTML 367 | } 368 | 369 | Splitting.html = html; 370 | Splitting.add = add; 371 | 372 | /** 373 | * Detects the grid by measuring which elements align to a side of it. 374 | * @param {!HTMLElement} el 375 | * @param {import('../core/types').ISplittingOptions} options 376 | * @param {*} side 377 | */ 378 | function detectGrid(el, options, side) { 379 | var items = $(options.matching || el.children, el); 380 | var c = {}; 381 | 382 | each(items, function(w) { 383 | var val = Math.round(w[side]); 384 | (c[val] || (c[val] = [])).push(w); 385 | }); 386 | 387 | return Object.keys(c).map(Number).sort(byNumber).map(selectFrom(c)); 388 | } 389 | 390 | /** 391 | * Sorting function for numbers. 392 | * @param {number} a 393 | * @param {number} b 394 | * @return {number} 395 | */ 396 | function byNumber(a, b) { 397 | return a - b; 398 | } 399 | 400 | var linePlugin = createPlugin( 401 | /* by= */ 'lines', 402 | /* depends= */ [WORDS], 403 | /* key= */ 'line', 404 | /* split= */ function(el, options, ctx) { 405 | return detectGrid(el, { matching: ctx[WORDS] }, 'offsetTop') 406 | } 407 | ); 408 | 409 | var itemPlugin = createPlugin( 410 | /* by= */ 'items', 411 | /* depends= */ _, 412 | /* key= */ 'item', 413 | /* split= */ function(el, options) { 414 | return $(options.matching || el.children, el) 415 | } 416 | ); 417 | 418 | var rowPlugin = createPlugin( 419 | /* by= */ 'rows', 420 | /* depends= */ _, 421 | /* key= */ 'row', 422 | /* split= */ function(el, options) { 423 | return detectGrid(el, options, "offsetTop"); 424 | } 425 | ); 426 | 427 | var columnPlugin = createPlugin( 428 | /* by= */ 'cols', 429 | /* depends= */ _, 430 | /* key= */ "col", 431 | /* split= */ function(el, options) { 432 | return detectGrid(el, options, "offsetLeft"); 433 | } 434 | ); 435 | 436 | var gridPlugin = createPlugin( 437 | /* by= */ 'grid', 438 | /* depends= */ ['rows', 'cols'] 439 | ); 440 | 441 | var LAYOUT = "layout"; 442 | 443 | var layoutPlugin = createPlugin( 444 | /* by= */ LAYOUT, 445 | /* depends= */ _, 446 | /* key= */ _, 447 | /* split= */ function(el, opts) { 448 | // detect and set options 449 | var rows = opts.rows = +(opts.rows || getData(el, 'rows') || 1); 450 | var columns = opts.columns = +(opts.columns || getData(el, 'columns') || 1); 451 | 452 | // Seek out the first if the value is true 453 | opts.image = opts.image || getData(el, 'image') || el.currentSrc || el.src; 454 | if (opts.image) { 455 | var img = $("img", el)[0]; 456 | opts.image = img && (img.currentSrc || img.src); 457 | } 458 | 459 | // add optional image to background 460 | if (opts.image) { 461 | setProperty(el, "background-image", "url(" + opts.image + ")"); 462 | } 463 | 464 | var totalCells = rows * columns; 465 | var elements = []; 466 | 467 | var container = createElement(_, "cell-grid"); 468 | while (totalCells--) { 469 | // Create a span 470 | var cell = createElement(container, "cell"); 471 | createElement(cell, "cell-inner"); 472 | elements.push(cell); 473 | } 474 | 475 | // Append elements back into the parent 476 | appendChild(el, container); 477 | 478 | return elements; 479 | } 480 | ); 481 | 482 | var cellRowPlugin = createPlugin( 483 | /* by= */ "cellRows", 484 | /* depends= */ [LAYOUT], 485 | /* key= */ "row", 486 | /* split= */ function(el, opts, ctx) { 487 | var rowCount = opts.rows; 488 | var result = Array2D(rowCount); 489 | 490 | each(ctx[LAYOUT], function(cell, i, src) { 491 | result[Math.floor(i / (src.length / rowCount))].push(cell); 492 | }); 493 | 494 | return result; 495 | } 496 | ); 497 | 498 | var cellColumnPlugin = createPlugin( 499 | /* by= */ "cellColumns", 500 | /* depends= */ [LAYOUT], 501 | /* key= */ "col", 502 | /* split= */ function(el, opts, ctx) { 503 | var columnCount = opts.columns; 504 | var result = Array2D(columnCount); 505 | 506 | each(ctx[LAYOUT], function(cell, i) { 507 | result[i % columnCount].push(cell); 508 | }); 509 | 510 | return result; 511 | } 512 | ); 513 | 514 | var cellPlugin = createPlugin( 515 | /* by= */ "cells", 516 | /* depends= */ ['cellRows', 'cellColumns'], 517 | /* key= */ "cell", 518 | /* split= */ function(el, opt, ctx) { 519 | // re-index the layout as the cells 520 | return ctx[LAYOUT]; 521 | } 522 | ); 523 | 524 | // install plugins 525 | // word/char plugins 526 | add(wordPlugin); 527 | add(charPlugin); 528 | add(linePlugin); 529 | // grid plugins 530 | add(itemPlugin); 531 | add(rowPlugin); 532 | add(columnPlugin); 533 | add(gridPlugin); 534 | // cell-layout plugins 535 | add(layoutPlugin); 536 | add(cellRowPlugin); 537 | add(cellColumnPlugin); 538 | add(cellPlugin); 539 | 540 | return Splitting; 541 | 542 | }))); 543 | -------------------------------------------------------------------------------- /dist/splitting.min.js: -------------------------------------------------------------------------------- 1 | !function(n,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):n.Splitting=t()}(this,function(){"use strict" 2 | var u=document,a=u.createTextNode.bind(u) 3 | function l(n,t,e){n.style.setProperty(t,e)}function f(n,t){return n.appendChild(t)}function d(n,t,e,r){var o=u.createElement("span") 4 | return t&&(o.className=t),e&&(r||o.setAttribute("data-"+t,e),o.textContent=e),n&&f(n,o)||o}function p(n,t){return n.getAttribute("data-"+t)}function m(n,t){return n&&0!=n.length?n.nodeName?[n]:[].slice.call(n[0].nodeName?n:(t||u).querySelectorAll(n)):[]}function i(n){for(var t=[];n--;)t[n]=[] 5 | return t}function h(n,t){n&&n.some(t)}function o(t){return function(n){return t[n]}}var c={} 6 | function n(n,t,e,r){return{by:n,depends:t,key:e,split:r}}function e(n){return function t(e,n,r){var o=r.indexOf(e) 7 | if(-1==o){r.unshift(e) 8 | var u=c[e] 9 | if(!u)throw new Error("plugin not loaded: "+e) 10 | h(u.depends,function(n){t(n,e,r)})}else u=r.indexOf(n),r.splice(o,1),r.splice(u,0,e) 11 | return r}(n,0,[]).map(o(c))}function t(n){c[n.by]=n}function g(n,e,r,o,u){n.normalize() 12 | var i=[],c=document.createDocumentFragment(),s=(o&&i.push(n.previousSibling),[]) 13 | return m(n.childNodes).some(function(n){var t 14 | n.tagName&&!n.hasChildNodes()?s.push(n):n.childNodes&&n.childNodes.length?(s.push(n),i.push.apply(i,g(n,e,r,o,u))):(t=(n=n.wholeText||"").trim()).length&&(" "===n[0]&&s.push(a(" ")),h(""===r&&"function"==typeof Intl.Segmenter?Array.from((new Intl.Segmenter).segment(t)).map(function(n){return n.segment}):t.split(r),function(n,t){t&&u&&s.push(d(c,"whitespace"," ",u)) 15 | t=d(c,e,n) 16 | i.push(t),s.push(t)})," "===n[n.length-1])&&s.push(a(" "))}),h(s,function(n){f(c,n)}),n.innerHTML="",f(n,c),i}var v=0 17 | var s="words",r=n(s,v,"word",function(n){return g(n,"word",/\s+/,0,1)}),y="chars",w=n(y,[s],"char",function(n,e,t){var r=[] 18 | return h(t[s],function(n,t){r.push.apply(r,g(n,"char","",e.whitespace&&t))}),r}) 19 | function b(t){var a=(t=t||{}).key 20 | return m(t.target||"[data-splitting]").map(function(i){var n,c,s=i["🍌"] 21 | return!t.force&&s||(s=i["🍌"]={el:i},n=e(n=(n=t.by||p(i,"splitting"))&&"true"!=n?n:y),c=function(n,t){for(var e in t)n[e]=t[e] 22 | return n}({},t),h(n,function(n){var t,e,r,o,u 23 | n.split&&(t=n.by,r=(a?"-"+a:"")+n.key,n=n.split(i,c,s),r&&(e=i,u=(r="--"+(r=r))+"-index",h(o=n,function(n,t){Array.isArray(n)?h(n,function(n){l(n,u,t)}):l(n,u,t)}),l(e,r+"-total",o.length)),s[t]=n,i.classList.add(t))}),i.classList.add("splitting")),s})}function N(n,t,e){var t=m(t.matching||n.children,n),r={} 24 | return h(t,function(n){var t=Math.round(n[e]);(r[t]||(r[t]=[])).push(n)}),Object.keys(r).map(Number).sort(x).map(o(r))}function x(n,t){return n-t}b.html=function(n){var t=(n=n||{}).target=d() 25 | return t.innerHTML=n.content,b(n),t.outerHTML} 26 | var S=n("lines",[s],"line",function(n,t,e){return N(n,{matching:e[s]},"offsetTop")}),T=n("items",v,"item",function(n,t){return m(t.matching||n.children,n)}),A=n("rows",v,"row",function(n,t){return N(n,t,"offsetTop")}),L=n("cols",v,"col",function(n,t){return N(n,t,"offsetLeft")}),k=n("grid",["rows","cols"]),C="layout",M=n(C,v,v,function(n,t){for(var e,r=t.rows=+(t.rows||p(n,"rows")||1),o=t.columns=+(t.columns||p(n,"columns")||1),u=(t.image=t.image||p(n,"image")||n.currentSrc||n.src,t.image&&(e=m("img",n)[0],t.image=e&&(e.currentSrc||e.src)),t.image&&l(n,"background-image","url("+t.image+")"),r*o),i=[],c=d(v,"cell-grid");u--;){var s=d(c,"cell") 27 | d(s,"cell-inner"),i.push(s)}return f(n,c),i}),H=n("cellRows",[C],"row",function(n,t,e){var r=t.rows,o=i(r) 28 | return h(e[C],function(n,t,e){o[Math.floor(t/(e.length/r))].push(n)}),o}),O=n("cellColumns",[C],"col",function(n,t,e){var r=t.columns,o=i(r) 29 | return h(e[C],function(n,t){o[t%r].push(n)}),o}),j=n("cells",["cellRows","cellColumns"],"cell",function(n,t,e){return e[C]}) 30 | return(b.add=t)(r),t(w),t(S),t(T),t(A),t(L),t(k),t(M),t(H),t(O),t(j),b}) 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "splitting", 3 | "version": "1.1.0", 4 | "description": "Micro-library to split a DOM element's words & chars into elements populated with CSS vars.", 5 | "main": "dist/splitting.js", 6 | "scripts": { 7 | "build": "npm run build:lite && npm run build:lite:compressed && npm run build:all && npm run build:all:compressed", 8 | "build:all": "rollup -c config/rollup.all.js", 9 | "build:all:compressed": "rollup -c config/rollup.all.compressed.js", 10 | "build:lite": "rollup -c config/rollup.lite.js", 11 | "build:lite:compressed": "rollup -c config/rollup.lite.compressed.js", 12 | "version": "npm run build && git add .", 13 | "test": "jest" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/shshaw/splitting.git" 18 | }, 19 | "files": [ 20 | "dist/*" 21 | ], 22 | "keywords": [ 23 | "split", 24 | "text", 25 | "char", 26 | "word", 27 | "splitting", 28 | "css", 29 | "vars" 30 | ], 31 | "author": "Stephen Shaw (stephen@brokensquare.com)", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/shshaw/splitting/issues" 35 | }, 36 | "homepage": "https://splitting.js.org", 37 | "devDependencies": { 38 | "babel-jest": "^23.4.0", 39 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 40 | "babel-preset-env": "^1.7.0", 41 | "jest": "^29.3.1", 42 | "jsdom": "^24.1.0", 43 | "rollup": "^0.55.5", 44 | "rollup-plugin-filesize": "^2.0.0", 45 | "rollup-plugin-uglify": "^4.0.0", 46 | "uglify-js": "^3.3.11" 47 | }, 48 | "jest": { 49 | "transform": { 50 | "^.+\\.js": "babel-jest" 51 | }, 52 | "moduleFileExtensions": [ 53 | "js" 54 | ], 55 | "testRegex": "tests/features/.*\\.js$", 56 | "setupTestFrameworkScriptFile": "./tests/_setup.js" 57 | }, 58 | "dependencies": {} 59 | } 60 | -------------------------------------------------------------------------------- /src/all.js: -------------------------------------------------------------------------------- 1 | import { Splitting } from "./core/splitting"; 2 | import { add } from "./core/plugin-manager"; 3 | 4 | import { wordPlugin } from "./plugins/words"; 5 | import { charPlugin } from "./plugins/chars"; 6 | import { linePlugin } from "./plugins/lines"; 7 | import { itemPlugin } from "./plugins/items"; 8 | import { rowPlugin } from "./plugins/rows"; 9 | import { columnPlugin } from "./plugins/columns"; 10 | import { gridPlugin } from "./plugins/grid"; 11 | import { layoutPlugin } from "./plugins/layout"; 12 | import { cellRowPlugin } from "./plugins/cellRows"; 13 | import { cellColumnPlugin } from "./plugins/cellColumns"; 14 | import { cellPlugin } from "./plugins/cells"; 15 | 16 | // install plugins 17 | // word/char plugins 18 | add(wordPlugin); 19 | add(charPlugin); 20 | add(linePlugin); 21 | // grid plugins 22 | add(itemPlugin); 23 | add(rowPlugin); 24 | add(columnPlugin); 25 | add(gridPlugin); 26 | // cell-layout plugins 27 | add(layoutPlugin); 28 | add(cellRowPlugin); 29 | add(cellColumnPlugin); 30 | add(cellPlugin); 31 | 32 | export default Splitting; 33 | -------------------------------------------------------------------------------- /src/core/plugin-manager.js: -------------------------------------------------------------------------------- 1 | import { selectFrom, each } from "../utils/arrays"; 2 | 3 | /** 4 | * @type {Record} 5 | */ 6 | var plugins = {}; 7 | 8 | /** 9 | * @param {string} by 10 | * @param {string} parent 11 | * @param {!Array} deps 12 | * @return {!Array} 13 | */ 14 | function resolvePlugins(by, parent, deps) { 15 | // skip if already visited this dependency 16 | var index = deps.indexOf(by); 17 | if (index == -1) { 18 | // if new to dependency array, add to the beginning 19 | deps.unshift(by); 20 | 21 | // recursively call this function for all dependencies 22 | var plugin = plugins[by]; 23 | if (!plugin) { 24 | throw new Error("plugin not loaded: " + by); 25 | } 26 | each(plugin.depends, function(p) { 27 | resolvePlugins(p, by, deps); 28 | }); 29 | } else { 30 | // if this dependency was added already move to the left of 31 | // the parent dependency so it gets loaded in order 32 | var indexOfParent = deps.indexOf(parent); 33 | deps.splice(index, 1); 34 | deps.splice(indexOfParent, 0, by); 35 | } 36 | return deps; 37 | } 38 | 39 | /** 40 | * Internal utility for creating plugins... essentially to reduce 41 | * the size of the library 42 | * @param {string} by 43 | * @param {string} key 44 | * @param {string[]} depends 45 | * @param {Function} split 46 | * @returns {import('./types').ISplittingPlugin} 47 | */ 48 | export function createPlugin(by, depends, key, split) { 49 | return { 50 | by: by, 51 | depends: depends, 52 | key: key, 53 | split: split 54 | } 55 | } 56 | 57 | /** 58 | * 59 | * @param {string} by 60 | * @returns {import('./types').ISplittingPlugin[]} 61 | */ 62 | export function resolve(by) { 63 | return resolvePlugins(by, 0, []).map(selectFrom(plugins)); 64 | } 65 | 66 | /** 67 | * Adds a new plugin to splitting 68 | * @param {import('./types').ISplittingPlugin} opts 69 | */ 70 | export function add(opts) { 71 | plugins[opts.by] = opts; 72 | } 73 | -------------------------------------------------------------------------------- /src/core/splitting.js: -------------------------------------------------------------------------------- 1 | import { $, createElement, getData } from '../utils/dom' 2 | import { index } from '../utils/css-vars' 3 | import { each } from '../utils/arrays' 4 | 5 | import { add, resolve } from './plugin-manager'; 6 | import { CHARS } from '../plugins/chars'; 7 | import { copy } from '../utils/objects'; 8 | 9 | /** 10 | * # Splitting 11 | * 12 | * @param {import('./types').ISplittingOptions} opts 13 | * @return {!Array<*>} 14 | */ 15 | export function Splitting (opts) { 16 | opts = opts || {}; 17 | var key = opts.key; 18 | 19 | return $(opts.target || '[data-splitting]').map(function(el) { 20 | var ctx = el['🍌']; 21 | if (!opts.force && ctx) { 22 | return ctx; 23 | } 24 | 25 | ctx = el['🍌'] = { el: el }; 26 | var by = opts.by || getData(el, 'splitting'); 27 | if (!by || by == 'true') { 28 | by = CHARS; 29 | } 30 | var items = resolve(by); 31 | var opts2 = copy({}, opts); 32 | each(items, function(plugin) { 33 | if (plugin.split) { 34 | var pluginBy = plugin.by; 35 | var key2 = (key ? '-' + key : '') + plugin.key; 36 | var results = plugin.split(el, opts2, ctx); 37 | key2 && index(el, key2, results); 38 | ctx[pluginBy] = results; 39 | el.classList.add(pluginBy); 40 | } 41 | }); 42 | 43 | el.classList.add('splitting'); 44 | return ctx; 45 | }) 46 | } 47 | 48 | /** 49 | * # Splitting.html 50 | * 51 | * @param {import('./types').ISplittingOptions} opts 52 | */ 53 | function html(opts) { 54 | opts = opts || {} 55 | var parent = opts.target = createElement(); 56 | parent.innerHTML = opts.content; 57 | Splitting(opts) 58 | return parent.outerHTML 59 | } 60 | 61 | Splitting.html = html; 62 | Splitting.add = add; 63 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | Splitting: ISplittingStatic 4 | } 5 | } 6 | 7 | export type Target = string | Node | NodeList | Element[]; 8 | 9 | export interface ISplittingPlugin { 10 | by: string; 11 | key?: string; 12 | depends?: string[]; 13 | split?: (el: HTMLElement, options?: ISplittingOptions) => HTMLElement[]; 14 | } 15 | 16 | export interface ISplittingStatic { 17 | (options?: ISplittingOptions): SplittingInstance[]; 18 | add(options?: ISplittingPlugin): void; 19 | html(options?: ISplittingOptions): string; 20 | } 21 | 22 | export interface SplittingInstance { 23 | el: Element; 24 | chars?: SplittingInstance[]; 25 | words?: SplittingInstance[]; 26 | lines?: SplittingInstance[]; 27 | items?: SplittingInstance[]; 28 | cols?: SplittingInstance[][]; 29 | rows?: SplittingInstance[][]; 30 | cells?: SplittingInstance[][]; 31 | cellColumns?: SplittingInstance[][]; 32 | cellRows?: SplittingInstance[][]; 33 | } 34 | 35 | export interface ISplittingOptions { 36 | target?: Target; 37 | by?: string; 38 | options?: Record 39 | } -------------------------------------------------------------------------------- /src/lite.js: -------------------------------------------------------------------------------- 1 | import { Splitting } from './core/splitting'; 2 | import { add } from './core/plugin-manager'; 3 | 4 | import { wordPlugin } from "./plugins/words"; 5 | import { charPlugin } from "./plugins/chars"; 6 | // import { linePlugin } from "./plugins/lines"; 7 | // import { itemPlugin } from "./plugins/items"; 8 | // import { rowPlugin } from "./plugins/rows"; 9 | // import { columnPlugin } from "./plugins/columns"; 10 | // import { gridPlugin } from "./plugins/grid"; 11 | // import { layoutPlugin } from "./plugins/layout"; 12 | // import { cellRowPlugin } from "./plugins/cellRows"; 13 | // import { cellColumnPlugin } from "./plugins/cellColumns"; 14 | // import { cellPlugin } from "./plugins/cells"; 15 | 16 | // install plugins 17 | // word/char plugins 18 | add(wordPlugin) 19 | add(charPlugin) 20 | // add(linePlugin) 21 | // grid plugins 22 | // add(itemPlugin) 23 | // add(rowPlugin) 24 | // add(columnPlugin) 25 | // add(gridPlugin) 26 | // cell-layout plugins 27 | // add(layoutPlugin) 28 | // add(cellRowPlugin) 29 | // add(cellColumnPlugin) 30 | // add(cellPlugin) 31 | 32 | export default Splitting; -------------------------------------------------------------------------------- /src/plugins/cellColumns.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from "../core/plugin-manager"; 2 | import { Array2D, each } from "../utils/arrays"; 3 | import { LAYOUT } from "./layout"; 4 | 5 | export var cellColumnPlugin = createPlugin( 6 | /* by= */ "cellColumns", 7 | /* depends= */ [LAYOUT], 8 | /* key= */ "col", 9 | /* split= */ function(el, opts, ctx) { 10 | var columnCount = opts.columns; 11 | var result = Array2D(columnCount); 12 | 13 | each(ctx[LAYOUT], function(cell, i) { 14 | result[i % columnCount].push(cell); 15 | }); 16 | 17 | return result; 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /src/plugins/cellRows.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '../core/plugin-manager'; 2 | import { Array2D, each } from "../utils/arrays"; 3 | import { LAYOUT } from './layout'; 4 | 5 | export var cellRowPlugin = createPlugin( 6 | /* by= */ "cellRows", 7 | /* depends= */ [LAYOUT], 8 | /* key= */ "row", 9 | /* split= */ function(el, opts, ctx) { 10 | var rowCount = opts.rows; 11 | var result = Array2D(rowCount); 12 | 13 | each(ctx[LAYOUT], function(cell, i, src) { 14 | result[Math.floor(i / (src.length / rowCount))].push(cell); 15 | }); 16 | 17 | return result; 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /src/plugins/cells.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '../core/plugin-manager'; 2 | import { _ } from '../utils/objects'; 3 | import { LAYOUT } from './layout'; 4 | 5 | export var cellPlugin = createPlugin( 6 | /* by= */ "cells", 7 | /* depends= */ ['cellRows', 'cellColumns'], 8 | /* key= */ "cell", 9 | /* split= */ function(el, opt, ctx) { 10 | // re-index the layout as the cells 11 | return ctx[LAYOUT]; 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /src/plugins/chars.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '../core/plugin-manager'; 2 | import { splitText } from "../utils/split-text"; 3 | import { each } from "../utils/arrays"; 4 | import { WORDS } from './words'; 5 | 6 | export var CHARS = "chars"; 7 | 8 | export var charPlugin = createPlugin( 9 | /* by= */ CHARS, 10 | /* depends= */ [WORDS], 11 | /* key= */ "char", 12 | /* split= */ function(el, options, ctx) { 13 | var results = []; 14 | 15 | each(ctx[WORDS], function(word, i) { 16 | results.push.apply(results, splitText(word, "char", "", options.whitespace && i)); 17 | }); 18 | 19 | return results; 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /src/plugins/columns.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '../core/plugin-manager'; 2 | import { detectGrid } from "../utils/detect-grid"; 3 | import { _ } from '../utils/objects'; 4 | 5 | export var columnPlugin = createPlugin( 6 | /* by= */ 'cols', 7 | /* depends= */ _, 8 | /* key= */ "col", 9 | /* split= */ function(el, options) { 10 | return detectGrid(el, options, "offsetLeft"); 11 | } 12 | ); 13 | 14 | -------------------------------------------------------------------------------- /src/plugins/grid.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '../core/plugin-manager'; 2 | 3 | export var gridPlugin = createPlugin( 4 | /* by= */ 'grid', 5 | /* depends= */ ['rows', 'cols'] 6 | ); 7 | -------------------------------------------------------------------------------- /src/plugins/items.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '../core/plugin-manager'; 2 | import { $ } from '../utils/dom'; 3 | import { _ } from '../utils/objects'; 4 | 5 | export var itemPlugin = createPlugin( 6 | /* by= */ 'items', 7 | /* depends= */ _, 8 | /* key= */ 'item', 9 | /* split= */ function(el, options) { 10 | return $(options.matching || el.children, el) 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /src/plugins/layout.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from "../core/plugin-manager"; 2 | import { $, createElement, appendChild, setProperty, getData } from "../utils/dom"; 3 | import { _ } from '../utils/objects'; 4 | 5 | export var LAYOUT = "layout"; 6 | 7 | export var layoutPlugin = createPlugin( 8 | /* by= */ LAYOUT, 9 | /* depends= */ _, 10 | /* key= */ _, 11 | /* split= */ function(el, opts) { 12 | // detect and set options 13 | var rows = opts.rows = +(opts.rows || getData(el, 'rows') || 1); 14 | var columns = opts.columns = +(opts.columns || getData(el, 'columns') || 1); 15 | 16 | // Seek out the first if the value is true 17 | opts.image = opts.image || getData(el, 'image') || el.currentSrc || el.src; 18 | if (opts.image) { 19 | var img = $("img", el)[0]; 20 | opts.image = img && (img.currentSrc || img.src); 21 | } 22 | 23 | // add optional image to background 24 | if (opts.image) { 25 | setProperty(el, "background-image", "url(" + opts.image + ")"); 26 | } 27 | 28 | var totalCells = rows * columns; 29 | var elements = []; 30 | 31 | var container = createElement(_, "cell-grid"); 32 | while (totalCells--) { 33 | // Create a span 34 | var cell = createElement(container, "cell"); 35 | createElement(cell, "cell-inner"); 36 | elements.push(cell); 37 | } 38 | 39 | // Append elements back into the parent 40 | appendChild(el, container); 41 | 42 | return elements; 43 | } 44 | ); 45 | -------------------------------------------------------------------------------- /src/plugins/lines.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '../core/plugin-manager'; 2 | import { detectGrid } from '../utils/detect-grid' 3 | import { WORDS } from './words'; 4 | 5 | export var linePlugin = createPlugin( 6 | /* by= */ 'lines', 7 | /* depends= */ [WORDS], 8 | /* key= */ 'line', 9 | /* split= */ function(el, options, ctx) { 10 | return detectGrid(el, { matching: ctx[WORDS] }, 'offsetTop') 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /src/plugins/rows.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '../core/plugin-manager'; 2 | import { detectGrid } from "../utils/detect-grid"; 3 | import { _ } from '../utils/objects'; 4 | 5 | export var rowPlugin = createPlugin( 6 | /* by= */ 'rows', 7 | /* depends= */ _, 8 | /* key= */ 'row', 9 | /* split= */ function(el, options) { 10 | return detectGrid(el, options, "offsetTop"); 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /src/plugins/words.js: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '../core/plugin-manager'; 2 | import { splitText } from '../utils/split-text'; 3 | import { _ } from '../utils/objects'; 4 | 5 | export var WORDS = 'words' 6 | 7 | export var wordPlugin = createPlugin( 8 | /* by= */ WORDS, 9 | /* depends= */ _, 10 | /* key= */ 'word', 11 | /* split= */ function(el) { 12 | return splitText(el, 'word', /\s+/, 0, 1) 13 | } 14 | ); 15 | -------------------------------------------------------------------------------- /src/utils/arrays.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates and fills an array with the value provided 3 | * @param {number} len 4 | * @param {() => T} valueProvider 5 | * @return {T} 6 | * @template T 7 | */ 8 | export function Array2D(len) { 9 | var a = []; 10 | for (; len--; ) { 11 | a[len] = [] 12 | } 13 | return a; 14 | } 15 | 16 | /** 17 | * A for loop wrapper used to reduce js minified size. 18 | * @param {!Array} items 19 | * @param {function(T):void} consumer 20 | * @template T 21 | */ 22 | export function each(items, consumer) { 23 | items && items.some(consumer); 24 | } 25 | 26 | /** 27 | * @param {T} obj 28 | * @return {function(string):*} 29 | * @template T 30 | */ 31 | export function selectFrom(obj) { 32 | return function (key) { 33 | return obj[key]; 34 | } 35 | } -------------------------------------------------------------------------------- /src/utils/css-vars.js: -------------------------------------------------------------------------------- 1 | import { setProperty } from "./dom"; 2 | import { each } from './arrays'; 3 | 4 | /** 5 | * # Splitting.index 6 | * Index split elements and add them to a Splitting instance. 7 | * 8 | * @param {HTMLElement} element 9 | * @param {string} key 10 | * @param {!Array | !Array>} items 11 | */ 12 | export function index(element, key, items) { 13 | var prefix = '--' + key; 14 | var cssVar = prefix + "-index"; 15 | 16 | each(items, function (items, i) { 17 | if (Array.isArray(items)) { 18 | each(items, function(item) { 19 | setProperty(item, cssVar, i); 20 | }); 21 | } else { 22 | setProperty(items, cssVar, i); 23 | } 24 | }); 25 | 26 | setProperty(element, prefix + "-total", items.length); 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/detect-grid.js: -------------------------------------------------------------------------------- 1 | import { selectFrom, each } from "./arrays"; 2 | import { $ } from './dom'; 3 | 4 | /** 5 | * Detects the grid by measuring which elements align to a side of it. 6 | * @param {!HTMLElement} el 7 | * @param {import('../core/types').ISplittingOptions} options 8 | * @param {*} side 9 | */ 10 | export function detectGrid(el, options, side) { 11 | var items = $(options.matching || el.children, el); 12 | var c = {}; 13 | 14 | each(items, function(w) { 15 | var val = Math.round(w[side]); 16 | (c[val] || (c[val] = [])).push(w); 17 | }); 18 | 19 | return Object.keys(c).map(Number).sort(byNumber).map(selectFrom(c)); 20 | } 21 | 22 | /** 23 | * Sorting function for numbers. 24 | * @param {number} a 25 | * @param {number} b 26 | * @return {number} 27 | */ 28 | function byNumber(a, b) { 29 | return a - b; 30 | } -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | export var root = document; 2 | export var createText = root.createTextNode.bind(root); 3 | 4 | /** 5 | * # setProperty 6 | * Apply a CSS var 7 | * @param {HTMLElement} el 8 | * @param {string} varName 9 | * @param {string|number} value 10 | */ 11 | export function setProperty(el, varName, value) { 12 | el.style.setProperty(varName, value); 13 | } 14 | 15 | /** 16 | * 17 | * @param {!HTMLElement} el 18 | * @param {!HTMLElement} child 19 | */ 20 | export function appendChild(el, child) { 21 | return el.appendChild(child); 22 | } 23 | 24 | /** 25 | * 26 | * @param {!HTMLElement} parent 27 | * @param {string} key 28 | * @param {string} text 29 | * @param {boolean} whitespace 30 | */ 31 | export function createElement(parent, key, text, whitespace) { 32 | var el = root.createElement('span'); 33 | key && (el.className = key); 34 | if (text) { 35 | !whitespace && el.setAttribute("data-" + key, text); 36 | el.textContent = text; 37 | } 38 | return (parent && appendChild(parent, el)) || el; 39 | } 40 | 41 | /** 42 | * 43 | * @param {!HTMLElement} el 44 | * @param {string} key 45 | */ 46 | export function getData(el, key) { 47 | return el.getAttribute("data-" + key) 48 | } 49 | 50 | /** 51 | * 52 | * @param {import('../types').Target} e 53 | * @param {!HTMLElement} parent 54 | * @returns {!Array} 55 | */ 56 | export function $(e, parent) { 57 | return !e || e.length == 0 58 | ? // null or empty string returns empty array 59 | [] 60 | : e.nodeName 61 | ? // a single element is wrapped in an array 62 | [e] 63 | : // selector and NodeList are converted to Element[] 64 | [].slice.call(e[0].nodeName ? e : (parent || root).querySelectorAll(e)); 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/objects.js: -------------------------------------------------------------------------------- 1 | /** an empty value */ 2 | export var _ = 0 3 | 4 | export function copy(dest, src) { 5 | for (var k in src) { 6 | dest[k] = src[k] 7 | } 8 | return dest; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/split-text.js: -------------------------------------------------------------------------------- 1 | import { $, appendChild, createElement, createText } from "./dom"; 2 | import { each } from "./arrays"; 3 | 4 | /** 5 | * # Splitting.split 6 | * Split an element's textContent into individual elements 7 | * @param {!HTMLElement} el Element to split 8 | * @param {string} key 9 | * @param {string} splitOn 10 | * @param {boolean} includePrevious 11 | * @param {boolean} preserveWhitespace 12 | * @return {!Array} 13 | */ 14 | export function splitText(el, key, splitOn, includePrevious, preserveWhitespace) { 15 | // Combine any strange text nodes or empty whitespace. 16 | el.normalize(); 17 | 18 | // Use fragment to prevent unnecessary DOM thrashing. 19 | var elements = []; 20 | var F = document.createDocumentFragment(); 21 | 22 | if (includePrevious) { 23 | elements.push(el.previousSibling); 24 | } 25 | 26 | var allElements = []; 27 | $(el.childNodes).some(function(next) { 28 | if (next.tagName && !next.hasChildNodes()) { 29 | // keep elements without child nodes (no text and no children) 30 | allElements.push(next); 31 | return; 32 | } 33 | // Recursively run through child nodes 34 | if (next.childNodes && next.childNodes.length) { 35 | allElements.push(next); 36 | elements.push.apply(elements, splitText(next, key, splitOn, includePrevious, preserveWhitespace)); 37 | return; 38 | } 39 | 40 | // Get the text to split, trimming out the whitespace 41 | /** @type {string} */ 42 | var wholeText = next.wholeText || ''; 43 | var contents = wholeText.trim(); 44 | 45 | // If there's no text left after trimming whitespace, continue the loop 46 | if (contents.length) { 47 | // insert leading space if there was one 48 | if (wholeText[0] === ' ') { 49 | allElements.push(createText(' ')); 50 | } 51 | // Concatenate the split text children back into the full array 52 | var useSegmenter = splitOn === "" && typeof Intl.Segmenter === "function"; 53 | each(useSegmenter ? Array.from(new Intl.Segmenter().segment(contents)).map(function(x){return x.segment}) : contents.split(splitOn), function (splitText, i) { 54 | if (i && preserveWhitespace) { 55 | allElements.push(createElement(F, "whitespace", " ", preserveWhitespace)); 56 | } 57 | var splitEl = createElement(F, key, splitText); 58 | elements.push(splitEl); 59 | allElements.push(splitEl); 60 | }); 61 | // insert trailing space if there was one 62 | if (wholeText[wholeText.length - 1] === ' ') { 63 | allElements.push(createText(' ')); 64 | } 65 | } 66 | }); 67 | 68 | each(allElements, function(el) { 69 | appendChild(F, el); 70 | }); 71 | 72 | // Clear out the existing element 73 | el.innerHTML = ""; 74 | appendChild(el, F); 75 | return elements; 76 | } 77 | -------------------------------------------------------------------------------- /tests/_setup.js: -------------------------------------------------------------------------------- 1 | Element.prototype.insertAdjacentElement = function(position, container) { 2 | var ref = this; 3 | var ref_parent = ref.parentNode; 4 | var node; 5 | var first_child; 6 | var next_sibling; 7 | 8 | switch (position.toLowerCase()) { 9 | case 'beforebegin': 10 | while ((node = container.firstChild)) { 11 | ref_parent.insertBefore(node, ref); 12 | } 13 | break; 14 | case 'afterbegin': 15 | first_child = ref.firstChild; 16 | while ((node = container.lastChild)) { 17 | first_child = ref.insertBefore(node, first_child); 18 | } 19 | break; 20 | case 'beforeend': 21 | while ((node = container.firstChild)) { 22 | ref.appendChild(node); 23 | } 24 | break; 25 | case 'afterend': 26 | next_sibling = ref.nextSibling; 27 | while ((node = container.lastChild)) { 28 | next_sibling = ref_parent.insertBefore(node, next_sibling); 29 | } 30 | break; 31 | } 32 | } 33 | 34 | Element.prototype.insertAdjacentText = function(position, html) { 35 | var container = this.ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', '_'); 36 | container.innerHTML = html; 37 | this.insertAdjacentElement(position, container); 38 | }; 39 | 40 | // polyfill css variables 41 | var originalSetProperty = CSSStyleDeclaration.prototype.setProperty; 42 | var originalPropValue = CSSStyleDeclaration.prototype.getPropertyValue; 43 | CSSStyleDeclaration.prototype.setProperty = function(key, value) { 44 | if (!key.startsWith('--')) { 45 | return originalSetProperty.call(this, key, value); 46 | } 47 | var maps = (this._maps || (this._maps = {})); 48 | maps[key] = value; 49 | } 50 | CSSStyleDeclaration.prototype.getPropertyValue = function(key) { 51 | return (this._maps || {})[key] || originalPropValue.call(this, key); 52 | } 53 | 54 | // MOCK the layout properties. These are not implemented in JSDOM 55 | Object.defineProperties(HTMLElement.prototype, { 56 | offsetLeft: { 57 | get: function() { return this._offsetLeft|| getComputedStyle(this).marginLeft; }, 58 | set: function(v) { this._offsetLeft = v; } 59 | }, 60 | offsetTop: { 61 | get: function() { return this._offsetTop || getComputedStyle(this).marginTop; }, 62 | set: function(v) { this._offsetTop = v; } 63 | }, 64 | offsetHeight: { 65 | get: function() { return this._offsetHeight || getComputedStyle(this).offsetHeight; }, 66 | set: function(v) { this._offsetHeight = v; } 67 | }, 68 | offsetWidth: { 69 | get: function() { return this._offsetWidth || getComputedStyle(this).offsetWidth; }, 70 | set: function(v) { this._offsetWidth = v; } 71 | } 72 | }); -------------------------------------------------------------------------------- /tests/features/splitting.cells.js: -------------------------------------------------------------------------------- 1 | import Splitting from '../../src/all' 2 | import { $create } from '../utils/dom'; 3 | 4 | test('creates a 4 x 3 grid correctly', function() { 5 | var el = $create` 6 |
7 | 8 |
9 | ` 10 | 11 | var actual = Splitting({ 12 | target: el, 13 | by: 'cells', 14 | rows: 4, 15 | columns: 3, 16 | image: true 17 | })[0]; 18 | 19 | expect(actual.el.style.backgroundImage).toBe('url(http://placehold.it/1/1)'); 20 | 21 | expect(actual.cells.length).toBe(12); 22 | expect(actual.cellColumns.length).toBe(3); 23 | expect(actual.cellRows.length).toBe(4); 24 | expect(actual.cells[0].classList.contains('cell')).toBeTruthy(); 25 | }); 26 | 27 | test('initializes multiple cell zones', function() { 28 | var els = [ 29 | $create`
30 | 31 |
`, 32 | $create`
33 | 34 |
` 35 | ]; 36 | 37 | debugger; 38 | 39 | var actual = Splitting({ 40 | target: els, 41 | image: true 42 | }); 43 | 44 | expect(actual[0].cells.length).toBe(3); 45 | expect(actual[0].cellRows.length).toBe(3); 46 | expect(actual[0].cellColumns.length).toBe(1); 47 | 48 | expect(actual[1].cells.length).toBe(4); 49 | expect(actual[1].cellColumns.length).toBe(4); 50 | expect(actual[1].cellRows.length).toBe(1); 51 | }); -------------------------------------------------------------------------------- /tests/features/splitting.chars.js: -------------------------------------------------------------------------------- 1 | import Splitting from '../../src/all'; 2 | import { $create } from '../utils/dom'; 3 | 4 | test('an empty element', function() { 5 | var $el = document.createElement('div'); 6 | 7 | var els = Splitting({ target: $el, by: 'chars' }); 8 | expect(els.length).toBe(1); 9 | expect(els[0].words.length).toBe(0); 10 | expect(els[0].chars.length).toBe(0); 11 | }); 12 | 13 | test('an element with a single character', function() { 14 | var el = $create`
C
` 15 | 16 | var actual = Splitting({ target: el, by: 'chars' }); 17 | expect(actual.length).toBe(1); 18 | expect(actual[0].words[0].textContent).toBe('C'); 19 | }); 20 | 21 | test('an element with a single word', function() { 22 | var $el = document.createElement('div'); 23 | $el.innerHTML = 'SPLITTING'; 24 | 25 | var els = Splitting({ target: $el, by: 'chars' }); 26 | expect(els.length).toBe(1); 27 | expect(els[0].words[0].textContent).toBe('SPLITTING'); 28 | }); 29 | 30 | test('an element with a multiple words', function() { 31 | var input = $create`
with multiple words
`; 32 | 33 | var actual = Splitting({ target: input, by: 'chars' }); 34 | expect(actual.length).toBe(1); 35 | expect(actual[0].words[0].textContent).toBe('with'); 36 | expect(actual[0].words[1].textContent).toBe('multiple'); 37 | expect(actual[0].words[2].textContent).toBe('words'); 38 | }); 39 | 40 | test('an element with a multiple words with spaces', function() { 41 | var input = document.createElement('div'); 42 | input.innerHTML = 'with many'; 43 | 44 | var actual = Splitting({ target: input, by: 'chars' }); 45 | expect(actual.length).toBe(1); 46 | expect(actual[0].chars.length).toBe(8); 47 | expect(actual[0].chars[0].textContent).toBe('w'); 48 | expect(actual[0].chars[1].textContent).toBe('i'); 49 | expect(actual[0].chars[2].textContent).toBe('t'); 50 | expect(actual[0].chars[3].textContent).toBe('h'); 51 | expect(actual[0].chars[4].textContent).toBe('m'); 52 | expect(actual[0].chars[5].textContent).toBe('a'); 53 | expect(actual[0].chars[6].textContent).toBe('n'); 54 | expect(actual[0].chars[7].textContent).toBe('y'); 55 | }); 56 | 57 | test('an element with a multiple words with spaces', function() { 58 | var input = document.createElement('div'); 59 | input.textContent = 'with many'; 60 | 61 | var actual = Splitting({ target: input, by: 'chars', whitespace: true }); 62 | expect(actual.length).toBe(1); 63 | expect(actual[0].chars[0].textContent).toBe('w'); 64 | expect(actual[0].chars[1].textContent).toBe('i'); 65 | expect(actual[0].chars[2].textContent).toBe('t'); 66 | expect(actual[0].chars[3].textContent).toBe('h'); 67 | expect(actual[0].chars[4].textContent).toBe(' '); 68 | expect(actual[0].chars[5].textContent).toBe('m'); 69 | expect(actual[0].chars[6].textContent).toBe('a'); 70 | expect(actual[0].chars[7].textContent).toBe('n'); 71 | expect(actual[0].chars[8].textContent).toBe('y'); 72 | }); 73 | 74 | test('a nested empty element', function() { 75 | var $el = document.createElement('div'); 76 | var $el2 = document.createElement('div'); 77 | $el.appendChild($el2) 78 | $el2.innerHTML = '' 79 | 80 | var results = Splitting({ target: $el, by: 'chars' }); 81 | 82 | expect(results.length).toBe(1) 83 | expect(results[0].words.length).toBe(0); 84 | }); 85 | 86 | test('an element with emojis', function () { 87 | var $el = document.createElement("div"); 88 | $el.innerHTML = "Hello 👨‍👩‍👧‍👦⭐️"; 89 | 90 | var results = Splitting({ target: $el, by: 'chars' }); 91 | 92 | expect(results.length).toBe(1); 93 | expect(results[0].chars.length).toBe(7); 94 | expect(results[0].words.length).toBe(2); 95 | }); 96 | 97 | test('a multi-level nested empty element', function() { 98 | // todo 99 | }); 100 | 101 | test('a multi-level nested element', function() { 102 | // todo 103 | }); 104 | 105 | test('retriggering on already split element', function() { 106 | // todo 107 | }); 108 | -------------------------------------------------------------------------------- /tests/features/splitting.grid.js: -------------------------------------------------------------------------------- 1 | import Splitting from '../../src/all'; 2 | import { $create } from '../utils/dom'; 3 | 4 | test('an empty element', function () { 5 | var el = document.createElement('div'); 6 | var results = Splitting({ target: el, by: 'grid' }); 7 | 8 | expect(results.length).toBe(1) 9 | expect(results[0].cols.length).toBe(0); 10 | expect(results[0].rows.length).toBe(0); 11 | }); 12 | 13 | test('an element with one element', function () { 14 | var el = $create` 15 |
16 | ` 17 | 18 | var results = Splitting({ target: el, by: 'grid' }); 19 | 20 | expect(results.length).toBe(1) 21 | expect(results[0].cols.length).toBe(1); 22 | expect(results[0].rows.length).toBe(1); 23 | }); 24 | 25 | test('an element with multiple elements', function () { 26 | var el = $create` 27 |
28 |
1
29 |
2
30 |
` 31 | 32 | el.children[1].offsetTop = 10; 33 | 34 | var results = Splitting({ target: el, by: 'grid' }); 35 | 36 | expect(results.length).toBe(1) 37 | expect(results[0].rows.length).toBe(2); 38 | expect(results[0].cols.length).toBe(1); 39 | }); 40 | 41 | test('an element with nested elements', function () { 42 | var el = $create` 43 |
44 |
1
45 |
2
46 |
47 | ` 48 | 49 | el.children[1].offsetTop = 10; 50 | 51 | document.body.appendChild(el); 52 | 53 | var results = Splitting({ target: el, by: 'grid', matching: '.item2' }); 54 | 55 | expect(results.length).toBe(1) 56 | expect(results[0].rows.length).toBe(2); 57 | expect(results[0].cols.length).toBe(1); 58 | 59 | document.body.removeChild(el); 60 | }); 61 | 62 | test('no child selector', function () { 63 | // todo 64 | }); 65 | 66 | test('no key', function () { 67 | // todo 68 | }); 69 | -------------------------------------------------------------------------------- /tests/features/splitting.html.js: -------------------------------------------------------------------------------- 1 | import Splitting from '../../src/all'; 2 | 3 | test('basic test', function () { 4 | const content = "
Hello World!
"; 5 | const actual = Splitting.html({ content }); 6 | 7 | // jsdom isn't outputting css custom properties, so this is as correct as we can go 8 | const expected = 9 | `` + 10 | '
' + 11 | '' + 12 | 'H' + 13 | 'e' + 14 | 'l' + 15 | 'l' + 16 | 'o' + 17 | '' + 18 | ' ' + 19 | '' + 20 | 'W' + 21 | 'o' + 22 | 'r' + 23 | 'l' + 24 | 'd' + 25 | '!' + 26 | '' + 27 | '
' + 28 | `
`; 29 | 30 | expect(actual).toBe(expected); 31 | }); 32 | 33 | test('splitting textContent works properly', () => { 34 | const content = "Hello!"; 35 | const actual = Splitting.html({ content }); 36 | 37 | const expected = `` 38 | + `` 39 | + `H` 40 | + `e` 41 | + `l` 42 | + `l` 43 | + `o` 44 | + `!` 45 | + `` 46 | + ``; 47 | 48 | expect(actual).toBe(expected); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/features/splitting.items.js: -------------------------------------------------------------------------------- 1 | import Splitting from '../../src/all'; 2 | 3 | test('an empty element', function () { 4 | var el = document.createElement('div'); 5 | var results = Splitting({ target: el, by: 'items' }); 6 | 7 | expect(results.length).toBe(1) 8 | expect(results[0].items.length).toBe(0); 9 | }); 10 | 11 | test('an element with one element', function () { 12 | var el = document.createElement('div'); 13 | var el2 = document.createElement('div'); 14 | el.appendChild(el2); 15 | var results = Splitting({ target: el, by: 'items' }); 16 | 17 | expect(results.length).toBe(1) 18 | expect(results[0].items.length).toBe(1); 19 | }); 20 | 21 | test('an element with multiple elements', function () { 22 | var el = document.createElement('div'); 23 | var el2 = document.createElement('div'); 24 | var el3 = document.createElement('div'); 25 | el.appendChild(el2); 26 | el.appendChild(el3); 27 | var results = Splitting({ target: el, by: 'items' }); 28 | 29 | expect(results.length).toBe(1) 30 | expect(results[0].items.length).toBe(2); 31 | }); 32 | 33 | test('an element with nested elements', function () { 34 | var el = document.createElement('div'); 35 | var el1 = document.createElement('div'); 36 | el.appendChild(el1); 37 | 38 | var el2 = document.createElement('div'); 39 | el2.classList.add('item') 40 | el1.appendChild(el2); 41 | 42 | var el3 = document.createElement('div'); 43 | el3.classList.add('item') 44 | el1.appendChild(el3); 45 | 46 | var results = Splitting({ target: el, by: 'items', matching: '.item' }); 47 | 48 | expect(results.length).toBe(1) 49 | expect(results[0].items.length).toBe(2); 50 | }); 51 | 52 | test('no child selector', function () { 53 | // todo 54 | }); 55 | 56 | test('no key', function () { 57 | // todo 58 | }); 59 | -------------------------------------------------------------------------------- /tests/features/splitting.js: -------------------------------------------------------------------------------- 1 | import Splitting from '../../src/all'; 2 | 3 | test("no arguments", function () { 4 | var result = Splitting(); 5 | expect(result).toEqual([]); 6 | }); 7 | 8 | 9 | test("passing an element", function () { 10 | var el = document.createElement("div"); 11 | var els = Splitting({ target: el }); 12 | expect(els.length).toEqual(1); 13 | expect(els[0].el).toBe(el); 14 | }); 15 | 16 | test("passing a nodelist", function () { 17 | 18 | var id = 'test-element'; 19 | var el = document.createElement("div"); 20 | el.setAttribute('id', id); 21 | document.body.appendChild(el); 22 | 23 | var els = Splitting({ target: document.querySelectorAll("#" + id) }); 24 | expect(els.length).toEqual(1); 25 | expect(els[0].el).toBe(el); 26 | 27 | document.body.removeChild(el); 28 | }); 29 | 30 | test("passing a class selector", function () { 31 | var className = "passing-class-selector" 32 | var el = document.createElement("div"); 33 | el.className = className; 34 | document.body.appendChild(el); 35 | 36 | var els = Splitting({ target: "." + className }); 37 | expect(els.length).toEqual(1); 38 | expect(els[0].el).toBe(el); 39 | 40 | document.body.removeChild(el); 41 | }); 42 | 43 | test("passing a non-existant selector", function () { 44 | 45 | var els = Splitting({ target: ".nonexistant-class-selector" }); 46 | expect(els.length).toEqual(0); 47 | 48 | }); 49 | 50 | test("passing an attribute selector", function () { 51 | var el = document.createElement("span"); 52 | el.setAttribute("data-attribute", true); 53 | document.body.appendChild(el); 54 | 55 | var els = Splitting({ target: "[data-attribute]" }); 56 | expect(els.length).toEqual(1); 57 | expect(els[0].el).toBe(el); 58 | 59 | document.body.removeChild(el); 60 | }); 61 | 62 | test("returns the same thing if split more than once", function () { 63 | var el = document.createElement("span"); 64 | el.innerHTML = "Hello World"; 65 | 66 | var els1 = Splitting({ target: el }); 67 | var els2 = Splitting({ target: el }); 68 | expect(els1[0]).toBe(els2[0]); 69 | }); 70 | 71 | test("returns a different thing if force split", function () { 72 | var el = document.createElement("span"); 73 | el.innerHTML = "Hello World"; 74 | 75 | var els1 = Splitting({ target: el, by: 'grid' }); 76 | var els2 = Splitting({ target: el, by: 'grid', force: true }); 77 | expect(els1[0]).not.toBe(els2[0]); 78 | }); 79 | 80 | test("A plugin of \"true\" is assumed to be the default value", () => { 81 | var el = document.createElement("span"); 82 | el.setAttribute("data-splitting", "true"); 83 | el.innerHTML = "TEST"; 84 | 85 | var els1 = Splitting({ target: el }); 86 | expect(els1[0].chars.length).toBe(4); 87 | }); 88 | 89 | test("throw a specific error when the plugin is not loaded", () => { 90 | var el = document.createElement("span"); 91 | el.setAttribute("data-splitting", "not-valid"); 92 | 93 | try { 94 | Splitting({ target: el }) 95 | throw new Error("did not throw"); 96 | } catch (err) { 97 | expect(err.message).toBe("plugin not loaded: not-valid") 98 | } 99 | }); -------------------------------------------------------------------------------- /tests/features/splitting.lines.js: -------------------------------------------------------------------------------- 1 | import Splitting from '../../src/all'; 2 | 3 | test('an empty element', function () { 4 | var $el = document.createElement('div') 5 | 6 | var els = Splitting({ target: $el, by: 'lines' }) 7 | expect(els.length).toBe(1) 8 | expect(els[0].lines.length).toBe(0) 9 | expect(els[0].words.length).toBe(0) 10 | }) 11 | 12 | test('an element with a single line', function () { 13 | var $el = document.createElement('div') 14 | $el.innerHTML = 'SPLITTING' 15 | 16 | var els = Splitting({ target: $el, by: 'lines' }) 17 | expect(els.length).toBe(1) 18 | expect(els[0].words[0].textContent).toBe('SPLITTING') 19 | }) 20 | 21 | test('a nested empty element', function () { 22 | // todo 23 | }) 24 | 25 | test('a multi-level nested empty element', function () { 26 | // todo 27 | }) 28 | 29 | test('a multi-level nested element', function () { 30 | // todo 31 | }) 32 | 33 | test('retriggering on already split element', function () { 34 | // todo 35 | }) 36 | -------------------------------------------------------------------------------- /tests/features/splitting.words.js: -------------------------------------------------------------------------------- 1 | import Splitting from "../../src/all"; 2 | 3 | test("an empty element", () => { 4 | var $el = document.createElement("div"); 5 | 6 | var els = Splitting({ target: $el, by: "words" }); 7 | expect(els.length).toBe(1); 8 | expect(els[0].words.length).toBe(0); 9 | }); 10 | 11 | test("an element with a single word", () => { 12 | var $el = document.createElement("div"); 13 | $el.innerHTML = "SPLITTING"; 14 | 15 | var els = Splitting({ target: $el, by: "words" }); 16 | expect(els.length).toBe(1); 17 | expect(els[0].words[0].textContent).toBe("SPLITTING"); 18 | }); 19 | 20 | test("an element with a multiple words", () => { 21 | var $el = document.createElement("div"); 22 | $el.innerHTML = "with multiple words"; 23 | 24 | var els = Splitting({ target: $el, by: "words" }); 25 | expect(els.length).toBe(1); 26 | expect(els[0].words.length).toBe(3); 27 | expect(els[0].words[0].textContent).toBe("with"); 28 | expect(els[0].words[1].textContent).toBe("multiple"); 29 | expect(els[0].words[2].textContent).toBe("words"); 30 | }); 31 | 32 | test("mixed content with spaces around words", () => { 33 | const input = '
Are We Good?
' 34 | const actual = Splitting.html({ content: input, by: 'words' }); 35 | // prettier-ignore 36 | const expected = 37 | `` 38 | + `
` 39 | + `Are` 40 | + ' ' // <- space preserved 41 | + `` 42 | + `We` 43 | + `` 44 | + ' ' // <- space preserved 45 | + `Good?` 46 | + `
` 47 | + `
`; 48 | 49 | expect(actual).toBe(expected); 50 | }); 51 | 52 | test("an element with a multiple words and emojis", () => { 53 | var $el = document.createElement("div"); 54 | $el.innerHTML = "with multiple words and 🤔 🍌bananas! 👨‍👩‍👧‍👦"; 55 | 56 | var els = Splitting({ target: $el, by: "words" }); 57 | expect(els.length).toBe(1); 58 | expect(els[0].words.length).toBe(7); 59 | expect(els[0].words[0].textContent).toBe("with"); 60 | expect(els[0].words[1].textContent).toBe("multiple"); 61 | expect(els[0].words[2].textContent).toBe("words"); 62 | expect(els[0].words[3].textContent).toBe("and"); 63 | expect(els[0].words[4].textContent).toBe("🤔"); 64 | expect(els[0].words[5].textContent).toBe("🍌bananas!"); 65 | expect(els[0].words[6].textContent).toBe("👨‍👩‍👧‍👦"); 66 | }); 67 | 68 | test("a nested empty element", () => { 69 | // todo 70 | }); 71 | 72 | test("a multi-level nested empty element", () => { 73 | // todo 74 | }); 75 | 76 | test("a multi-level nested element", () => { 77 | // todo 78 | }); 79 | 80 | test("retriggering on already split element", () => { 81 | // todo 82 | }); 83 | -------------------------------------------------------------------------------- /tests/utils/dom.js: -------------------------------------------------------------------------------- 1 | export function $create(content) { 2 | var el = document.createElement('div'); 3 | el.innerHTML = content; 4 | return el.firstElementChild; 5 | } --------------------------------------------------------------------------------