splitting.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. /*!
  2. Splitting
  3. Version: 1.0.5
  4. Plugin URL: https://splitting.js.org/
  5. License: Copyright © 2018-present Stephen Shaw | Licensed under the MIT license
  6. !*/
  7. (function (global, factory) {
  8. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  9. typeof define === 'function' && define.amd ? define(factory) :
  10. (global.Splitting = factory());
  11. }(this, (function () { 'use strict';
  12. var root = document;
  13. var createText = root.createTextNode.bind(root);
  14. /**
  15. * # setProperty
  16. * Apply a CSS var
  17. * @param {HTMLElement} el
  18. * @param {string} varName
  19. * @param {string|number} value
  20. */
  21. function setProperty(el, varName, value) {
  22. el.style.setProperty(varName, value);
  23. }
  24. /**
  25. *
  26. * @param {!HTMLElement} el
  27. * @param {!HTMLElement} child
  28. */
  29. function appendChild(el, child) {
  30. return el.appendChild(child);
  31. }
  32. /**
  33. *
  34. * @param {!HTMLElement} parent
  35. * @param {string} key
  36. * @param {string} text
  37. * @param {boolean} whitespace
  38. */
  39. function createElement(parent, key, text, whitespace) {
  40. var el = root.createElement('span');
  41. key && (el.className = key);
  42. if (text) {
  43. !whitespace && el.setAttribute("data-" + key, text);
  44. el.textContent = text;
  45. }
  46. return (parent && appendChild(parent, el)) || el;
  47. }
  48. /**
  49. *
  50. * @param {!HTMLElement} el
  51. * @param {string} key
  52. */
  53. function getData(el, key) {
  54. return el.getAttribute("data-" + key)
  55. }
  56. /**
  57. *
  58. * @param {import('../types').Target} e
  59. * @param {!HTMLElement} parent
  60. * @returns {!Array<!HTMLElement>}
  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. * Creates and fills an array with the value provided
  74. * @param {number} len
  75. * @param {() => T} valueProvider
  76. * @return {T}
  77. * @template T
  78. */
  79. function Array2D(len) {
  80. var a = [];
  81. for (; len--; ) {
  82. a[len] = [];
  83. }
  84. return a;
  85. }
  86. /**
  87. * A for loop wrapper used to reduce js minified size.
  88. * @param {!Array<T>} items
  89. * @param {function(T):void} consumer
  90. * @template T
  91. */
  92. function each(items, consumer) {
  93. items && items.some(consumer);
  94. }
  95. /**
  96. * @param {T} obj
  97. * @return {function(string):*}
  98. * @template T
  99. */
  100. function selectFrom(obj) {
  101. return function (key) {
  102. return obj[key];
  103. }
  104. }
  105. /**
  106. * # Splitting.index
  107. * Index split elements and add them to a Splitting instance.
  108. *
  109. * @param {HTMLElement} element
  110. * @param {string} key
  111. * @param {!Array<!HTMLElement> | !Array<!Array<!HTMLElement>>} items
  112. */
  113. function index(element, key, items) {
  114. var prefix = '--' + key;
  115. var cssVar = prefix + "-index";
  116. each(items, function (items, i) {
  117. if (Array.isArray(items)) {
  118. each(items, function(item) {
  119. setProperty(item, cssVar, i);
  120. });
  121. } else {
  122. setProperty(items, cssVar, i);
  123. }
  124. });
  125. setProperty(element, prefix + "-total", items.length);
  126. }
  127. /**
  128. * @type {Record<string, import('./types').ISplittingPlugin>}
  129. */
  130. var plugins = {};
  131. /**
  132. * @param {string} by
  133. * @param {string} parent
  134. * @param {!Array<string>} deps
  135. * @return {!Array<string>}
  136. */
  137. function resolvePlugins(by, parent, deps) {
  138. // skip if already visited this dependency
  139. var index = deps.indexOf(by);
  140. if (index == -1) {
  141. // if new to dependency array, add to the beginning
  142. deps.unshift(by);
  143. // recursively call this function for all dependencies
  144. var plugin = plugins[by];
  145. if (!plugin) {
  146. throw new Error("plugin not loaded: " + by);
  147. }
  148. each(plugin.depends, function(p) {
  149. resolvePlugins(p, by, deps);
  150. });
  151. } else {
  152. // if this dependency was added already move to the left of
  153. // the parent dependency so it gets loaded in order
  154. var indexOfParent = deps.indexOf(parent);
  155. deps.splice(index, 1);
  156. deps.splice(indexOfParent, 0, by);
  157. }
  158. return deps;
  159. }
  160. /**
  161. * Internal utility for creating plugins... essentially to reduce
  162. * the size of the library
  163. * @param {string} by
  164. * @param {string} key
  165. * @param {string[]} depends
  166. * @param {Function} split
  167. * @returns {import('./types').ISplittingPlugin}
  168. */
  169. function createPlugin(by, depends, key, split) {
  170. return {
  171. by: by,
  172. depends: depends,
  173. key: key,
  174. split: split
  175. }
  176. }
  177. /**
  178. *
  179. * @param {string} by
  180. * @returns {import('./types').ISplittingPlugin[]}
  181. */
  182. function resolve(by) {
  183. return resolvePlugins(by, 0, []).map(selectFrom(plugins));
  184. }
  185. /**
  186. * Adds a new plugin to splitting
  187. * @param {import('./types').ISplittingPlugin} opts
  188. */
  189. function add(opts) {
  190. plugins[opts.by] = opts;
  191. }
  192. /**
  193. * # Splitting.split
  194. * Split an element's textContent into individual elements
  195. * @param {!HTMLElement} el Element to split
  196. * @param {string} key
  197. * @param {string} splitOn
  198. * @param {boolean} includePrevious
  199. * @param {boolean} preserveWhitespace
  200. * @return {!Array<!HTMLElement>}
  201. */
  202. function splitText(el, key, splitOn, includePrevious, preserveWhitespace) {
  203. // Combine any strange text nodes or empty whitespace.
  204. el.normalize();
  205. // Use fragment to prevent unnecessary DOM thrashing.
  206. var elements = [];
  207. var F = document.createDocumentFragment();
  208. if (includePrevious) {
  209. elements.push(el.previousSibling);
  210. }
  211. var allElements = [];
  212. $(el.childNodes).some(function(next) {
  213. if (next.tagName && !next.hasChildNodes()) {
  214. // keep elements without child nodes (no text and no children)
  215. allElements.push(next);
  216. return;
  217. }
  218. // Recursively run through child nodes
  219. if (next.childNodes && next.childNodes.length) {
  220. allElements.push(next);
  221. elements.push.apply(elements, splitText(next, key, splitOn, includePrevious, preserveWhitespace));
  222. return;
  223. }
  224. // Get the text to split, trimming out the whitespace
  225. /** @type {string} */
  226. var wholeText = next.wholeText || '';
  227. var contents = wholeText.trim();
  228. // If there's no text left after trimming whitespace, continue the loop
  229. if (contents.length) {
  230. // insert leading space if there was one
  231. if (wholeText[0] === ' ') {
  232. allElements.push(createText(' '));
  233. }
  234. // Concatenate the split text children back into the full array
  235. each(contents.split(splitOn), function(splitText, i) {
  236. if (i && preserveWhitespace) {
  237. allElements.push(createElement(F, "whitespace", " ", preserveWhitespace));
  238. }
  239. var splitEl = createElement(F, key, splitText);
  240. elements.push(splitEl);
  241. allElements.push(splitEl);
  242. });
  243. // insert trailing space if there was one
  244. if (wholeText[wholeText.length - 1] === ' ') {
  245. allElements.push(createText(' '));
  246. }
  247. }
  248. });
  249. each(allElements, function(el) {
  250. appendChild(F, el);
  251. });
  252. // Clear out the existing element
  253. el.innerHTML = "";
  254. appendChild(el, F);
  255. return elements;
  256. }
  257. /** an empty value */
  258. var _ = 0;
  259. function copy(dest, src) {
  260. for (var k in src) {
  261. dest[k] = src[k];
  262. }
  263. return dest;
  264. }
  265. var WORDS = 'words';
  266. var wordPlugin = createPlugin(
  267. /* by= */ WORDS,
  268. /* depends= */ _,
  269. /* key= */ 'word',
  270. /* split= */ function(el) {
  271. return splitText(el, 'word', /\s+/, 0, 1)
  272. }
  273. );
  274. var CHARS = "chars";
  275. var charPlugin = createPlugin(
  276. /* by= */ CHARS,
  277. /* depends= */ [WORDS],
  278. /* key= */ "char",
  279. /* split= */ function(el, options, ctx) {
  280. var results = [];
  281. each(ctx[WORDS], function(word, i) {
  282. results.push.apply(results, splitText(word, "char", "", options.whitespace && i));
  283. });
  284. return results;
  285. }
  286. );
  287. /**
  288. * # Splitting
  289. *
  290. * @param {import('./types').ISplittingOptions} opts
  291. * @return {!Array<*>}
  292. */
  293. function Splitting (opts) {
  294. opts = opts || {};
  295. var key = opts.key;
  296. return $(opts.target || '[data-splitting]').map(function(el) {
  297. var ctx = el['🍌'];
  298. if (!opts.force && ctx) {
  299. return ctx;
  300. }
  301. ctx = el['🍌'] = { el: el };
  302. var by = opts.by || getData(el, 'splitting');
  303. if (!by || by == 'true') {
  304. by = CHARS;
  305. }
  306. var items = resolve(by);
  307. var opts2 = copy({}, opts);
  308. each(items, function(plugin) {
  309. if (plugin.split) {
  310. var pluginBy = plugin.by;
  311. var key2 = (key ? '-' + key : '') + plugin.key;
  312. var results = plugin.split(el, opts2, ctx);
  313. key2 && index(el, key2, results);
  314. ctx[pluginBy] = results;
  315. el.classList.add(pluginBy);
  316. }
  317. });
  318. el.classList.add('splitting');
  319. return ctx;
  320. })
  321. }
  322. /**
  323. * # Splitting.html
  324. *
  325. * @param {import('./types').ISplittingOptions} opts
  326. */
  327. function html(opts) {
  328. opts = opts || {};
  329. var parent = opts.target = createElement();
  330. parent.innerHTML = opts.content;
  331. Splitting(opts);
  332. return parent.outerHTML
  333. }
  334. Splitting.html = html;
  335. Splitting.add = add;
  336. /**
  337. * Detects the grid by measuring which elements align to a side of it.
  338. * @param {!HTMLElement} el
  339. * @param {import('../core/types').ISplittingOptions} options
  340. * @param {*} side
  341. */
  342. function detectGrid(el, options, side) {
  343. var items = $(options.matching || el.children, el);
  344. var c = {};
  345. each(items, function(w) {
  346. var val = Math.round(w[side]);
  347. (c[val] || (c[val] = [])).push(w);
  348. });
  349. return Object.keys(c).map(Number).sort(byNumber).map(selectFrom(c));
  350. }
  351. /**
  352. * Sorting function for numbers.
  353. * @param {number} a
  354. * @param {number} b
  355. * @return {number}
  356. */
  357. function byNumber(a, b) {
  358. return a - b;
  359. }
  360. var linePlugin = createPlugin(
  361. /* by= */ 'lines',
  362. /* depends= */ [WORDS],
  363. /* key= */ 'line',
  364. /* split= */ function(el, options, ctx) {
  365. return detectGrid(el, { matching: ctx[WORDS] }, 'offsetTop')
  366. }
  367. );
  368. var itemPlugin = createPlugin(
  369. /* by= */ 'items',
  370. /* depends= */ _,
  371. /* key= */ 'item',
  372. /* split= */ function(el, options) {
  373. return $(options.matching || el.children, el)
  374. }
  375. );
  376. var rowPlugin = createPlugin(
  377. /* by= */ 'rows',
  378. /* depends= */ _,
  379. /* key= */ 'row',
  380. /* split= */ function(el, options) {
  381. return detectGrid(el, options, "offsetTop");
  382. }
  383. );
  384. var columnPlugin = createPlugin(
  385. /* by= */ 'cols',
  386. /* depends= */ _,
  387. /* key= */ "col",
  388. /* split= */ function(el, options) {
  389. return detectGrid(el, options, "offsetLeft");
  390. }
  391. );
  392. var gridPlugin = createPlugin(
  393. /* by= */ 'grid',
  394. /* depends= */ ['rows', 'cols']
  395. );
  396. var LAYOUT = "layout";
  397. var layoutPlugin = createPlugin(
  398. /* by= */ LAYOUT,
  399. /* depends= */ _,
  400. /* key= */ _,
  401. /* split= */ function(el, opts) {
  402. // detect and set options
  403. var rows = opts.rows = +(opts.rows || getData(el, 'rows') || 1);
  404. var columns = opts.columns = +(opts.columns || getData(el, 'columns') || 1);
  405. // Seek out the first <img> if the value is true
  406. opts.image = opts.image || getData(el, 'image') || el.currentSrc || el.src;
  407. if (opts.image) {
  408. var img = $("img", el)[0];
  409. opts.image = img && (img.currentSrc || img.src);
  410. }
  411. // add optional image to background
  412. if (opts.image) {
  413. setProperty(el, "background-image", "url(" + opts.image + ")");
  414. }
  415. var totalCells = rows * columns;
  416. var elements = [];
  417. var container = createElement(_, "cell-grid");
  418. while (totalCells--) {
  419. // Create a span
  420. var cell = createElement(container, "cell");
  421. createElement(cell, "cell-inner");
  422. elements.push(cell);
  423. }
  424. // Append elements back into the parent
  425. appendChild(el, container);
  426. return elements;
  427. }
  428. );
  429. var cellRowPlugin = createPlugin(
  430. /* by= */ "cellRows",
  431. /* depends= */ [LAYOUT],
  432. /* key= */ "row",
  433. /* split= */ function(el, opts, ctx) {
  434. var rowCount = opts.rows;
  435. var result = Array2D(rowCount);
  436. each(ctx[LAYOUT], function(cell, i, src) {
  437. result[Math.floor(i / (src.length / rowCount))].push(cell);
  438. });
  439. return result;
  440. }
  441. );
  442. var cellColumnPlugin = createPlugin(
  443. /* by= */ "cellColumns",
  444. /* depends= */ [LAYOUT],
  445. /* key= */ "col",
  446. /* split= */ function(el, opts, ctx) {
  447. var columnCount = opts.columns;
  448. var result = Array2D(columnCount);
  449. each(ctx[LAYOUT], function(cell, i) {
  450. result[i % columnCount].push(cell);
  451. });
  452. return result;
  453. }
  454. );
  455. var cellPlugin = createPlugin(
  456. /* by= */ "cells",
  457. /* depends= */ ['cellRows', 'cellColumns'],
  458. /* key= */ "cell",
  459. /* split= */ function(el, opt, ctx) {
  460. // re-index the layout as the cells
  461. return ctx[LAYOUT];
  462. }
  463. );
  464. // install plugins
  465. // word/char plugins
  466. add(wordPlugin);
  467. add(charPlugin);
  468. add(linePlugin);
  469. // grid plugins
  470. add(itemPlugin);
  471. add(rowPlugin);
  472. add(columnPlugin);
  473. add(gridPlugin);
  474. // cell-layout plugins
  475. add(layoutPlugin);
  476. add(cellRowPlugin);
  477. add(cellColumnPlugin);
  478. add(cellPlugin);
  479. return Splitting;
  480. })));