├── .eslintignore ├── src ├── index.js ├── utils │ └── index.js └── read-smore.js ├── docs └── src │ ├── fav.png │ ├── meta.jpg │ ├── read-smore-dood.jpg │ ├── app.js │ ├── app.scss │ └── index.html ├── .gitignore ├── .npmignore ├── .prettierrc ├── .eslintrc.json ├── LICENSE ├── package.json └── readme.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReadSmore from './read-smore.js' 2 | export default ReadSmore 3 | -------------------------------------------------------------------------------- /docs/src/fav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenscaff/read-smore/HEAD/docs/src/fav.png -------------------------------------------------------------------------------- /docs/src/meta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenscaff/read-smore/HEAD/docs/src/meta.jpg -------------------------------------------------------------------------------- /docs/src/read-smore-dood.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenscaff/read-smore/HEAD/docs/src/read-smore-dood.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # System-level 2 | .DS_Store 3 | 4 | # App stuff 5 | .sass-cache 6 | .parcel-cache 7 | npm-debug.log 8 | node_modules 9 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .DS_Store 4 | .parcel-cache 5 | .tmp 6 | .gitignore 7 | *.log 8 | npm-debug.log 9 | demo 10 | src 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "trailingComma": "none", 4 | "printWidth": 80, 5 | "singleQuote": true, 6 | "semi": false, 7 | "jsxBracketSameLine": false 8 | } 9 | -------------------------------------------------------------------------------- /docs/src/app.js: -------------------------------------------------------------------------------- 1 | import ReadSmore from '../../src' 2 | 3 | const readMores = document.querySelectorAll('.js-read-smore') 4 | // eslint-disable-next-line new-cap 5 | const RMs = ReadSmore(readMores) 6 | 7 | RMs.init() 8 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Get Character Count 5 | * @param {string 6 | * @param {number} 7 | */ 8 | export function getCharCount(str) { 9 | return str.length 10 | } 11 | 12 | /** 13 | * Get Word Count 14 | * @param {string} 15 | * @param {number} 16 | */ 17 | export function getWordCount(str) { 18 | const words = removeTags(str).split(' ') 19 | return words.filter((word) => word.trim() !== '').length 20 | } 21 | 22 | /** 23 | * Trim whitespace 24 | * @param {string} 25 | * @param {string} 26 | */ 27 | export function trimSpaces(str) { 28 | return str.replace(/(^\s*)|(\s*$)/gi, '') 29 | } 30 | 31 | /** 32 | * Remove HTML Tags from string 33 | * @param {string} 34 | * @param {string} 35 | */ 36 | export function removeTags(str) { 37 | if (str === null || str === '') { 38 | return false 39 | } 40 | 41 | return str.replace(/<[^>]+>/g, '') 42 | } 43 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "ecmaVersion": 2020, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "impliedStrict": true 8 | } 9 | }, 10 | "env": { 11 | "browser": true, 12 | "es6": true 13 | }, 14 | "rules": { 15 | "eqeqeq": [ 16 | "error", 17 | "always" 18 | ], 19 | "no-console": "off", 20 | "no-undefined": "off", 21 | "indent": [ 22 | "error", 23 | 2 24 | ], 25 | "quotes": [ 26 | "warn", 27 | "single" 28 | ], 29 | "no-multi-spaces": [ 30 | "warn", 31 | { 32 | "exceptions": { 33 | "VariableDeclarator": true 34 | } 35 | } 36 | ], 37 | "no-trailing-spaces": "warn", 38 | "new-cap": "warn", 39 | "no-redeclare": [ 40 | "error", 41 | { 42 | "builtinGlobals": true 43 | } 44 | ], 45 | "no-var": 1, 46 | "semi": [ 47 | 0, 48 | "always" 49 | ] 50 | } 51 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Stephen Scaff 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "read-smore", 3 | "version": "2.5.0", 4 | "description": "A simple read more / read less feature in vanilla js", 5 | "author": "Stephen Scaff ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/stephenscaff/read-smore" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/stephenscaff/read-smore/issues" 13 | }, 14 | "homepage": "https://stephenscaff.github.io/read-smore", 15 | "keywords": [ 16 | "read smore", 17 | "readsmore", 18 | "read more", 19 | "readmore.js", 20 | "read-more-js", 21 | "ellipse", 22 | "truncate", 23 | "content", 24 | "more content" 25 | ], 26 | "main": "dist/index.js", 27 | "unpkg": "dist/index.umd.js", 28 | "files": [ 29 | "dist" 30 | ], 31 | "targets": { 32 | "main": false, 33 | "module": false, 34 | "browser": false 35 | }, 36 | "scripts": { 37 | "clean": "rm -rf dist", 38 | "build": "microbundle", 39 | "dev": "npm run demo:start && microbundle watch", 40 | "demo:start": "parcel ./docs/src/*.html --dist-dir ./docs/dist", 41 | "demo:build": "parcel build ./docs/src/index.html --dist-dir ./docs/dist --public-url ./", 42 | "demo:deploy": "npm run demo:build && gh-pages -d ./docs/dist", 43 | "demo:clean": "rm -rf ./docs/dist", 44 | "lint": "eslint ./src/**js --fix " 45 | }, 46 | "devDependencies": { 47 | "@parcel/transformer-sass": "^2.10.0", 48 | "eslint": "^8.52.0", 49 | "gh-pages": "^5.0.0", 50 | "microbundle": "^0.15.1", 51 | "parcel": "^2.10.0" 52 | }, 53 | "type": "module" 54 | } 55 | -------------------------------------------------------------------------------- /docs/src/app.scss: -------------------------------------------------------------------------------- 1 | //@import '../../src/read-smore.css'; 2 | 3 | $mq-md: 32em; 4 | 5 | :root { 6 | --font-base: 'Work Sans', sans-serif; 7 | --font-mono: 'IBM Plex Mono', monaco, monospace; 8 | --font-size-code: 0.85em; 9 | --font-size-md: 1em; 10 | --color-dark: #2d130d; 11 | --color-light: #e7d0b2; 12 | --color-pink-light: #ffeaf2; 13 | --color-pink-mid: #d0828f; 14 | --color-pink-dark: #8a0060; 15 | --mq-md: 32em; 16 | 17 | @media (min-width: $mq-md) { 18 | --font-size-md: 1.1em; 19 | } 20 | } 21 | 22 | *, body, html { 23 | margin: 0; 24 | padding:0; 25 | line-height: 1.5; 26 | } 27 | 28 | 29 | html, body { 30 | background-color: #fff; 31 | font-family: var(--font-base); 32 | color: var(--color-dark); 33 | } 34 | 35 | html { 36 | scroll-behavior: smooth; 37 | overflow-x:hidden; 38 | } 39 | 40 | body { 41 | font-size: var(--font-size-md); 42 | } 43 | 44 | .sr-only { 45 | position:absolute; 46 | left:-10000px; 47 | top:auto; 48 | width:1px; 49 | height:1px; 50 | overflow:hidden; 51 | } 52 | 53 | h2, h3 { 54 | font-size: 1.35em; 55 | color: var(--color-dark); 56 | margin-bottom: 1em; 57 | @media (min-width: $mq-md) { 58 | font-size: 1.5em; 59 | } 60 | } 61 | 62 | h4 { 63 | font-size: 1.2em; 64 | color: var(--color-dark); 65 | margin-bottom: 1em; 66 | } 67 | 68 | p { 69 | margin-bottom: 1.2em; 70 | color: #3e3f42; 71 | line-height: 1.6; 72 | } 73 | 74 | a { 75 | color: var(--color-pink-mid); 76 | text-decoration:none; 77 | transition: color 0.4s ease; 78 | 79 | &:hover { 80 | color: var(--color-pink-dark); 81 | } 82 | } 83 | 84 | ul { 85 | padding-left: 1em; 86 | 87 | li { 88 | margin-bottom: 0.25em; 89 | } 90 | } 91 | 92 | code { 93 | font-family: var(--font-mono); 94 | font-size: var(--font-size-code); 95 | } 96 | 97 | .code-inline { 98 | background: var(--color-light); 99 | color: var(--color-dark); 100 | border-radius: 5px; 101 | padding: 0.25em 0.5rem; 102 | font-size: calc(var(--font-size-code) - 0.1em); 103 | } 104 | 105 | // Code Block 106 | .code-block { 107 | background: var(--color-dark); 108 | color: var(--color-light); 109 | margin: 0 -1.8em; 110 | padding: 1em 1.8em; 111 | border-radius: 0; 112 | overflow-x: hidden; 113 | 114 | @media (min-width: $mq-md) { 115 | padding: 2em; 116 | border-radius: 0.5rem; 117 | margin: 0 auto; 118 | } 119 | 120 | &__pre { 121 | overflow-x: auto; 122 | font-family: var(--font-mono); 123 | white-space: pre-wrap; 124 | } 125 | 126 | &.is-lg { 127 | padding: 2em; 128 | } 129 | } 130 | 131 | // Table Blocks 132 | .table-block { 133 | overflow-x:auto; 134 | -webkit-overflow-scrolling: touch; 135 | 136 | @media (max-width: $mq-md) { 137 | margin: 0 -1.8em; 138 | padding-left: 1.8em; 139 | padding-right: 1.8em; 140 | } 141 | } 142 | 143 | // Tables 144 | table { 145 | padding: 1em 0; 146 | 147 | th { 148 | text-align: left; 149 | padding-bottom: 1rem; 150 | border-bottom: 1px solid; 151 | } 152 | 153 | td { 154 | padding: 1em; 155 | text-align: left; 156 | padding: 1em 1em 1em 0em; 157 | vertical-align: top; 158 | border-bottom: 1px solid var(--color-light); 159 | 160 | &:nth-of-type(3){ 161 | min-width: 12em; 162 | } 163 | 164 | &:nth-of-type(4){ 165 | min-width: 6em; 166 | } 167 | } 168 | } 169 | 170 | // Page Grid / Container 171 | .grid { 172 | margin: 0 auto; 173 | width: 92%; 174 | max-width: 42em; 175 | } 176 | 177 | // Spacings 178 | .space-xs { 179 | padding: 1em 0; 180 | } 181 | 182 | .space-sm { 183 | padding: 2em 0; 184 | 185 | & + & { 186 | padding-top: 0; 187 | } 188 | } 189 | 190 | // Spacing section 191 | .space-md { 192 | padding: 4em 0; 193 | 194 | & + & { 195 | padding-top: 0; 196 | } 197 | } 198 | 199 | // Dividers 200 | .sep { 201 | display: block; 202 | border-top: 1px solid #eee; 203 | border-bottom: 0; 204 | } 205 | 206 | 207 | // Page Layout 208 | .page { 209 | position: relative; 210 | margin: 0 auto; 211 | padding: 2em 0; 212 | overflow-x: hidden; 213 | @media (min-width: $mq-md) { 214 | padding: 4em 0; 215 | } 216 | } 217 | 218 | // Page Header 219 | .page-header { 220 | padding: 1em 0; 221 | 222 | &__grid { 223 | display: flex; 224 | flex-direction: row-reverse; 225 | justify-content: space-between; 226 | 227 | @media (min-width: $mq-md) { 228 | display: flex; 229 | align-items: center; 230 | flex-direction: row; 231 | justify-content: flex-start; 232 | } 233 | } 234 | 235 | &__img { 236 | max-width: 5em; 237 | @media (min-width: $mq-md) { 238 | max-width: 8em; 239 | } 240 | } 241 | 242 | &__heading { 243 | padding-right: 2em;; 244 | @media (min-width: $mq-md) { 245 | padding-left: 2em; 246 | padding-right: 0; 247 | } 248 | } 249 | 250 | &__pretitle { 251 | display:block; 252 | } 253 | &__title { 254 | font-size: 1.5rem; 255 | margin-bottom: 0.5rem; 256 | @media (min-width: $mq-md) { 257 | font-size: 2rem; 258 | } 259 | } 260 | 261 | &__text { 262 | margin-bottom: 0.5em; 263 | } 264 | } 265 | 266 | // Page Footer 267 | .page-footer { 268 | padding: 3rem 0; 269 | border-top: 1px solid #eee; 270 | text-align: center; 271 | 272 | &__icon { 273 | width: 2em; 274 | margin: 0 auto; 275 | } 276 | } 277 | 278 | // Section Header + anchors 279 | .section-header { 280 | position: relative; 281 | display: block; 282 | margin-bottom: 1em; 283 | 284 | &__title { 285 | display: inline; 286 | } 287 | 288 | &__link { 289 | background-repeat: no-repeat; 290 | display: inline-block; 291 | height: 1rem; 292 | width: 1rem; 293 | } 294 | 295 | &__icon { 296 | left: 3px; 297 | position: relative; 298 | 299 | @media (min-width: $mq-md) { 300 | position: absolute; 301 | left: -22px; 302 | top: 8px; 303 | } 304 | } 305 | } 306 | 307 | // Post Blocks (for demos) 308 | .post { 309 | & + & { 310 | padding-top: 3em; 311 | margin-top: 3em; 312 | border-top: 1px solid #ddd; 313 | } 314 | 315 | &__title { 316 | font-size: 1.35em; 317 | margin-bottom: 0.5em; 318 | } 319 | 320 | &__meta { 321 | display: inline-flex; 322 | padding: 6px 8px; 323 | margin: 0 0 1rem; 324 | background: var(--color-pink-light); 325 | color: var(--color-pink-dark); 326 | border-radius: 1rem; 327 | font-family: var(--font-mono); 328 | font-size: 0.7rem; 329 | text-transform: uppercase; 330 | letter-spacing: 0.1rem; 331 | line-height: 1; 332 | } 333 | 334 | ul + p { 335 | margin-top: 1em; 336 | } 337 | } 338 | 339 | // Read Smore styles 340 | .read-smore { 341 | :not([data-read-smore-inline="true"]) + &__link-wrap { 342 | display: block; 343 | margin-top: 1em; 344 | } 345 | &__link { 346 | font-weight: 700; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/read-smore.js: -------------------------------------------------------------------------------- 1 | import { getWordCount, getCharCount, trimSpaces } from './utils' 2 | ;('use strict') 3 | 4 | const defaultOptions = { 5 | blockClassName: 'read-smore', 6 | wordsCount: 30, 7 | charsCount: null, 8 | moreText: 'Read More', 9 | lessText: 'Read Less', 10 | isInline: false, 11 | linkElement: 'a' 12 | } 13 | 14 | /** 15 | * ReadSmore 16 | * A simple Read More / Read Less js plugin that maintains origial markup. 17 | * 18 | * @author @stephenscaff 19 | * @param {HTML element} elements 20 | * @param {Object} options 21 | * @returns 22 | */ 23 | function ReadSmore(elements, options) { 24 | options = { ...defaultOptions, ...options } 25 | 26 | // Internal Settings 27 | let settings = { 28 | originalContentArr: [], 29 | truncatedContentArr: [] 30 | } 31 | 32 | /** 33 | * Init plugin 34 | * Loop over instances and begin truncation procress 35 | * @public 36 | */ 37 | function init() { 38 | elements.forEach((element, idx) => { 39 | truncate(element, idx) 40 | }) 41 | } 42 | 43 | /** 44 | * Is Characters 45 | * Utility to check if is chars mode 46 | * 47 | * @private 48 | * @param {HTML Elmenent} el - single element instance 49 | */ 50 | function isChars(el) { 51 | return ( 52 | el.dataset.readSmoreChars !== undefined || options.charsCount !== null 53 | ) 54 | } 55 | 56 | /** 57 | * Is inline option 58 | * @private 59 | * @param {HTML element} el - element instance 60 | * @returns {Bool} 61 | */ 62 | function isInline(el) { 63 | return el.dataset.readSmoreInline !== undefined || options.isInline === true 64 | } 65 | 66 | /** 67 | * Get Count of characters or words. 68 | * Favors Characters from data att, then option, then words. 69 | * @private 70 | * @param {HTML Elmenent} el - single element instance 71 | * @returns {Number} 72 | */ 73 | function getCount(el) { 74 | return ( 75 | parseInt(el.dataset.readSmoreChars) || 76 | parseInt(options.charsCount) || 77 | parseInt(el.dataset.readSmoreWords) || 78 | parseInt(options.wordsCount) 79 | ) 80 | } 81 | 82 | /** 83 | * Ellpise Content 84 | * Handles content ellipse by words or charactes 85 | * @private 86 | * @param {String} str - content string. 87 | * @param {Number} max - Number of words||chars2 to show before truncation. 88 | * @param {Bool} isChars - is by chars 89 | */ 90 | function ellipse(str, max, isChars = false) { 91 | const trimmedSpaces = trimSpaces(str) 92 | 93 | if (isChars) { 94 | return trimmedSpaces.slice(0, max - 1) + '...' 95 | } 96 | 97 | const words = trimmedSpaces.split(/\s+/) 98 | return words.slice(0, max - 1).join(' ') + '...' 99 | } 100 | 101 | /** 102 | * Truncate logic 103 | * Gets user defined count for words/chars (set by data att, option or default), 104 | * gets content's count by words/chars, if defined is less than content, truncate 105 | * @private 106 | * @param {HTML Elmenent} el - single element instance 107 | * @param {Number} idx - current instance index 108 | */ 109 | function truncate(el, idx) { 110 | const definedCount = getCount(el) 111 | const originalContent = el.innerHTML 112 | const isCharMode = isChars(el) 113 | const truncateContent = ellipse(originalContent, definedCount, isCharMode) 114 | const originalContentCount = isCharMode 115 | ? getCharCount(originalContent) 116 | : getWordCount(originalContent) 117 | 118 | settings.originalContentArr.push(originalContent) 119 | settings.truncatedContentArr.push(truncateContent) 120 | 121 | if (definedCount < originalContentCount) { 122 | el.innerHTML = settings.truncatedContentArr[idx] 123 | createLink(idx) 124 | } 125 | } 126 | 127 | /** 128 | * Creates and Inserts Read More Link 129 | * @private 130 | * @param {Number} idx - index reference of looped item 131 | */ 132 | function createLink(idx) { 133 | const isInlineLink = isInline(elements[idx]) 134 | const linkWrap = document.createElement('span') 135 | linkWrap.className = `${options.blockClassName}__link-wrap` 136 | linkWrap.innerHTML = linkTmpl(elements[idx]) 137 | 138 | if (isInlineLink) { 139 | handleInlineStyles(elements[idx], linkWrap) 140 | } 141 | elements[idx].after(linkWrap) 142 | setupToggleEvents(idx, isInlineLink) 143 | } 144 | 145 | /** 146 | * Read More Link Template 147 | * @param {HTML Element} el 148 | * @returns {String} - html string 149 | */ 150 | function linkTmpl(el) { 151 | const moreTextData = el.dataset.readSmoreMoreText 152 | const moreText = moreTextData || options.moreText 153 | return ` 154 | <${options.linkElement} 155 | class="${options.blockClassName}__link" 156 | style="cursor:pointer" 157 | aria-expanded="false" 158 | tabIndex="0"> 159 | ${moreText} 160 | 161 | ` 162 | } 163 | 164 | /** 165 | * Sets up and calls click and keyup (enter key) events 166 | * @private 167 | * @param {Number} idx - index of clicked link 168 | * @param {Bool} isInlineLink - if link element is inline with content 169 | */ 170 | function setupToggleEvents(idx, isInlineLink) { 171 | const link = elements[idx].nextSibling.firstElementChild 172 | link.addEventListener('click', (event) => 173 | handleToggle(event, idx, isInlineLink) 174 | ) 175 | link.addEventListener('keyup', (event) => { 176 | if (event.keyCode === 13 && options.linkElement === 'a') 177 | handleToggle(event, idx, isInlineLink) 178 | }) 179 | } 180 | 181 | /** 182 | * Toggle event 183 | * @private 184 | * @param {Event} event - click | keyup event 185 | * @param {Number} idx - index of clicked link 186 | * @param {Bool} isInlineLink - if link element is inline with content 187 | */ 188 | function handleToggle(event, idx, isInlineLink) { 189 | const moreTextData = elements[idx].dataset.readSmoreMoreText 190 | const lessTextData = elements[idx].dataset.readSmoreLessText 191 | const target = event.currentTarget 192 | const clicked = target.dataset.clicked === 'true' 193 | 194 | elements[idx].classList.toggle('is-expanded') 195 | elements[idx].innerHTML = clicked 196 | ? settings.truncatedContentArr[idx] 197 | : settings.originalContentArr[idx] 198 | target.innerHTML = clicked 199 | ? moreTextData || options.moreText 200 | : lessTextData || options.lessText 201 | target.dataset.clicked = !clicked 202 | target.ariaExpanded = !clicked 203 | 204 | if (isInlineLink) handleInlineStyles(elements[idx]) 205 | } 206 | 207 | /** 208 | * Add styles for inline option 209 | * @private 210 | * @param {HTML Elmenent} el - single element instance 211 | * @param {HTML Elmenent} link - link wrapper element 212 | */ 213 | function handleInlineStyles(el, link) { 214 | if (el) { 215 | el.lastElementChild.style.display = 'inline' 216 | el.style.display = 'inline' 217 | } 218 | if (link) link.style.display = 'inline' 219 | } 220 | 221 | // API 222 | return { 223 | init: init 224 | } 225 | } 226 | 227 | export default ReadSmore 228 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Read-Smore 2 | 3 | (cause read-more was already taken 😉) 4 | 5 | A customizable, lightweight vanilla JS plugin for truncating content with a Read more / Read less move, whilst preserving the original markup. Able to truncate by max word or character count. 6 | 7 | [Docs / Demo](https://stephenscaff.github.io/read-smore/) 8 | 9 |
10 | 11 | ## Contents 12 | 13 | 1. [📌 Features](#-features) 14 | 2. [🎯 Quickstart](#-quickstart) 15 | 3. [🧬 Options](#-options) 16 | 4. [🤖 Commands](#-commands) 17 | 5. [🕹️ Usage](#-usage) 18 | 6. [📓 Notes](#-notes) 19 | 7. [📅 To Dos](#-to-dos) 20 | 21 |
22 | 23 | ## 📌 Features 24 | 25 | - Super lightweight, no dependencies, vanilla js, es6. 26 | - Supports truncating content by max word or character count. 27 | - Define max word or characters via data attribute or option 28 | - Adds ellipse after truncated content. 29 | - Preserves existing markup (nice). 30 | - Read more / Read less text is customizable, via option or data-attribute. 31 | - Block level class name is customizable. 32 | - Read More link can be inlined with truncated content, or as block level element below. 33 | - No CSS deps, lib is 100% js. 34 | - Hybrid NPM Module, supporting `import` and `require` 35 | 36 |
37 | 38 | ## 🎯 Quickstart 39 | 40 | #### 1. Install from NPM 41 | 42 | `npm i read-smore` 43 | 44 | #### 2. Create markup with defined max words 45 | 46 | ``` 47 |
51 |

Stuff and words and stuff and words.

52 |

Words and stuff and words and stuff.

53 | 54 |
55 | ``` 56 | 57 | #### 3. Add JS and init 58 | 59 | ``` 60 | import ReadSmore from 'read-smore' 61 | 62 | // target all read more elements 63 | const readMores = document.querySelectorAll('.js-read-smore') 64 | 65 | // Init 66 | ReadSmore(readMores).init() 67 | ``` 68 | 69 | **Or, by require** 70 | 71 | ``` 72 | const ReadSmore = require("read-smore"); 73 | const readMores = document.querySelectorAll(".js-read-smore"); 74 | ReadSmore(readMores).init(); 75 | ``` 76 | 77 | **Or, by CDN** 78 | 79 | To include via CDN, find the latest UMD version at [https://unpkg.com/read-smore](https://unpkg.com/read-smore) and inlcude via script tag, like so: 80 | 81 | ``` 82 | 83 | ``` 84 | 85 | **Then, initialize** 86 | 87 | ``` 88 | const ReadSmore = window.readSmore 89 | 90 | // target all read more elements 91 | const readMoreEls = document.querySelectorAll('.js-read-smore') 92 | 93 | // Init 94 | ReadSmore(readMoreEls).init() 95 | ``` 96 | 97 |
98 | 99 | ## 🧬 Options 100 | 101 | `ReadSmore()` accepts a single options param, which supports the following properties: 102 | 103 | | Option | Type | Description | Default | 104 | | -------------- | ------- | ----------------------------------------------------- | ------------ | 105 | | blockClassName | String | BEM style block name for injected link template | `read-smore` | 106 | | lessText | String | 'Read Less' closer link text (if no data attribute) | `Read more` | 107 | | moreText | String | 'Read More' expander link text (if no data attribute) | `Read less` | 108 | | wordsCount | Number | Default max words (if no data attribute) | `30` | 109 | | charsCount | Number | Default max characters (if no data attribute) | `null` | 110 | | isInline | Boolean | Styles link text inline with content | `false` | 111 | | linkElement | String | Change expander element | `a` | 112 | 113 |
114 | 115 | ## 🤖 Project Commands 116 | 117 | #### Install Project Deps 118 | 119 | `npm i` 120 | 121 | #### Build 122 | 123 | `npm run build` 124 | 125 | Builds `src` with `microbundle` to the various common js patterns. 126 | 127 | #### Run Dev 128 | 129 | `npm run dev` 130 | 131 | Dev has microbundle begin watching / building the files, while also running the demo project via Parcel, which imports from the local src directory. 132 | 133 | #### Run Demo 134 | 135 | `npm run demo:start` 136 | 137 | #### Lint 138 | 139 | `npm run lint` 140 | 141 |
142 | 143 | ## 🕹️ Usage 144 | 145 | #### Init JS 146 | 147 | ``` 148 | import ReadSmore from 'read-smore' 149 | 150 | // target all read more elements with `js-read-smore` class 151 | const readMores = document.querySelectorAll('.js-read-smore') 152 | 153 | // Init 154 | ReadSmore(readMores).init() 155 | ``` 156 | 157 | #### Example by max word count 158 | 159 | To truncate content by max **word** count, use the data attribute `data-read-smore-words=""` with desired value. 160 | 161 | ``` 162 |
166 |

Stuff and words and stuff and words.

167 |

Words and stuff and words and stuff.

168 | 169 |
170 | ``` 171 | 172 | #### Example by max character count 173 | 174 | To truncate content by max **character** count, use the data attribute `data-read-smore-chars=""` with desired value. 175 | 176 | ``` 177 |
181 |

Stuff and words and stuff and words.

182 |

Words and stuff and words and stuff.

183 | 184 |
185 | ``` 186 | 187 | #### Example defining read more/less text via data attribute 188 | 189 | To truncate content by max **character** count, use the data attribute `data-read-smore-chars=""` with desired value. 190 | 191 | ``` 192 |
198 |

Stuff and words and stuff and words.

199 |

Words and stuff and words and stuff.

200 | 201 |
202 | ``` 203 | 204 | #### Provide Options 205 | 206 | ReadSmore supports a few options, such as editing the more/less text. See Options table below for more. 207 | 208 | ``` 209 | import ReadSmore from 'read-smore' 210 | 211 | const readMores = document.querySelectorAll('.js-read-smore') 212 | 213 | const options = { 214 | blockClassName: 'read-more', 215 | moreText: 'Peep more', 216 | lessText: 'Peep less' 217 | } 218 | 219 | // Pass in options param 220 | ReadSmore(readMores, options).init() 221 | ``` 222 | 223 | #### Inline Read More link 224 | 225 | You can have the Read More link appear inline with the ellipsed content, as opposed to below it. 226 | 227 | Note: As of v2.2.0, required css dep was removed in favor of a pure js approach that simply applied inline styles. 228 | 229 | **1: Via `data-read-smore-inline`** 230 | 231 | ``` 232 |
237 |

Stuff and words and stuff and words.

238 |

Words and stuff and words and stuff.

239 | 240 |
241 | ``` 242 | 243 | **2: Via Option (effects all readSmore instances** 244 | 245 | ``` 246 | const readMores = document.querySelectorAll('.js-read-smore') 247 | 248 | const options = { 249 | isInline: true 250 | } 251 | 252 | const RMs = ReadSmore(readMores, options) 253 | ``` 254 | 255 |
256 | 257 | ## 📓 Notes 258 | 259 | - Need to figure out how to handle ReadMore instances with ajaxed/Fetched in content, since the word count on existing instances will be already truncated. 260 | - Thinking the solution is to destroy and rebuild via a click event. Or, at least open all and rebuild on click. 261 | 262 |
263 | 264 | ## 📅 To Dos 265 | 266 | - ~~Overhaul and simplfiy API to be more plugin / module like~~ 267 | - ~~Rename everything to 'ReadSmore'~~ 268 | - ~~Add docs / demo pages via gh-pages~~ 269 | - ~~Bundle as Hybrid NPM Module to support `import` and `require`~~ 270 | - ~~Remove CSS needed for inline option~~ 271 | - Provide callbacks on open/close 272 | - Provide a destroy method 273 | - Provide a solution for content injected after page load 274 | - Add some tests 275 | -------------------------------------------------------------------------------- /docs/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ReadSmore.js 9 | 13 | 17 | 21 | 25 | 29 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | 70 | 71 |
72 |
73 |

About

76 | 77 | 82 | About Section 83 | 84 |
85 |
87 |

ReadSmore.js is a customizable, lightweight and dependency-free JS library for truncating content with a Read more / Read less link, whilst preserving original markup. The plugin supports truncation by max word or character count, defined via data attribute or options.

88 |

Read Smore is authored by Stephen Scaff. View it on Github and NPM

89 |

Just like the example you're checking out right here. You can checkout more examples here.

90 |
91 |
92 | 93 | 94 |
95 |
96 |

Features

99 | 100 | 105 | Features Section 106 | 107 |
108 |
    109 |
  • Super duper lightweight, no dependencies, vanilla js.
  • 110 |
  • Supports truncating content by max Word or Character count.
  • 111 |
  • Adds ellipse after truncated content.
  • 112 |
  • Preserves existing markup (nice).
  • 113 |
  • Read more / Read less text is customizable.
  • 114 |
  • Use data attributes to control max words/characters count
  • 115 |
  • Block level class name is customizable
  • 116 |
  • Read More text can be block level or inline via provided css
  • 117 |
  • Hybrid NPM Module supporting import and require
  • 118 |
119 |
120 | 121 | 122 |
123 |
124 |

Install

127 | 128 | 133 | Install Section 134 | 135 |
136 |
npm i read-smore
137 |
138 | 139 | 140 |
141 |
142 |

Usage: Setup

145 | 146 | 151 | Usage and Setup Section 152 | 153 |
154 |

Init ReadSmore()

155 |
156 |
157 | import ReadSmore from 'read-smore'
158 | 
159 | // target all read smore elements
160 | const readMores = document.querySelectorAll('.js-read-smore')
161 | 
162 | // init
163 | ReadSmore(readMores).init()
164 |                 
165 |
166 | 167 |
168 | 169 |

Or by require

170 |
171 |
172 | const ReadSmore = require('read-smore')
173 | const readMores = document.querySelectorAll('.js-read-smore')
174 | ReadSmore(readMores).init()
175 |                 
176 |
177 | 178 |
179 | 180 |

Or by CDN

181 |

To include via CDN, find the latest UMD version at https://unpkg.com/read-smore and inlcude via script tag, like so:

182 |
183 |
184 | // index.html
185 | <script src="https://unpkg.com/read-smore@2.0.4/dist/index.umd.js">
186 |                 
187 |
188 | 189 |
190 | 191 |
192 |
193 | // Init JS
194 | const ReadSmore = window.readSmore
195 | 
196 | // target all read more elements
197 | const readMoreEls = document.querySelectorAll('.js-read-smore')
198 | 
199 | // Init
200 | ReadSmore(readMoreEls).init()
201 |                 
202 |
203 | 204 |
205 |
206 |
207 |

Usage: Markup

210 | 211 | 216 | Usage - Markup Section 217 | 218 |
219 |

Example by max word count (via data-read-smore-words)

220 |

To truncate content by max word count, use the data attribute data-read-smore-words="" with desired value.

221 |
222 |
223 | <div 
224 |   class="js-read-smore" 
225 |   data-read-smore-words="70" 
226 | >
227 |   <p>Stuff and words and stuff and words.</p>
228 |   <p>Words and stuff and words and stuff.</p>
229 |   <!-- more HTML content -->
230 | </div>
231 |                   
232 |
233 |
234 | 235 |
236 |

Example by max character count (via data-read-smore-chars)

237 |

To truncate content by max character count, use the data attribute
data-read-smore-chars="" with desired value. 238 |

239 |
240 | <div 
241 |   class="js-read-smore" 
242 |   data-read-smore-chars="150" 
243 | >
244 |   <p>Stuff and words and stuff and words.</p>
245 |   <p>Words and stuff and words and stuff.</p>
246 |   <!-- more HTML content -->
247 | </div>
248 |                 
249 |
250 |
251 | 252 | 253 |
254 |

Example setting more/less text via data attributes

255 |

To set custom more/less text, use the data attributes
data-read-smore-more-text | data-read-smore-less-text with desired value. 256 |

This overrides the values set via option.moreText | option.lessText for that instance.

257 |
258 |
259 | <div 
260 | class="js-read-smore" 
261 | data-read-smore-chars="150" 
262 | data-read-smore-more-text="Learn Schmore"
263 | data-read-smore-less-text="Learn Schless"
264 | >
265 | <p>Stuff and words and stuff and words.</p>
266 | <p>Words and stuff and words and stuff.</p>
267 | <!-- more HTML content -->
268 | </div>
269 |               
270 |
271 |
272 | 273 |
274 |

Inline Read more

275 |

By default the Read more text sits below the content block. However, you can make it inlined with the ellipsed text

276 |
277 |

1. Provide the the data-attribute data-read-smore-inline="true"

278 | 279 |
280 |
281 | <div 
282 |   class="js-read-smore" 
283 |   data-read-smore-words="70" 
284 |   data-read-smore-inline="true">
285 | </div>
286 |                 
287 |
288 | 289 |
290 | 291 |

2. Or pass as option (effects all readSmore instances) 292 | 293 |

294 |
295 | const readMores = document.querySelectorAll('.js-read-smore')
296 | 
297 | const options = {
298 |   isInline: true
299 | }
300 | 
301 | const RMs = ReadSmore(readMores, options)
302 | 
303 | 
304 |
305 |
306 |
307 | 308 |
309 |
310 |

Options

313 | 314 | 319 | Options S 320 | 321 |
322 |
323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 |
OptionTypeDescriptionDefault
335 | blockClassName 336 | StringSets Block level/parent class nameread-smore
moreTextStringSets "Read more" text (if no data attribute)Read more
lessTextStringSets "Read less" text (if no data attribute)Read less
wordsCountNumberSets max word count. Read Smore defaults to by word if nothing is defined70
charsCountNumberSets max character count.null
isInlineBooleanSets Read more link as inline with contentfalse
373 |
374 |
375 | 376 |
377 |

Passing Options

378 |

ReadSmore supports a few options, such as editing the more/less text. See Options table below for more.

379 |
380 |
381 | import ReadSmore from 'read-smore'
382 | 
383 | const readMores = document.querySelectorAll('.js-read-smore')
384 | 
385 | const options = {
386 |   blockClassName: 'read-more',
387 |   moreText: 'Custom more text',
388 |   lessText: 'Custom less text'
389 | }
390 | 
391 | ReadSmore(readMores, options).init()
392 |                   
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |

Examples

404 | 405 | 410 | Examples Section 411 | 412 |
413 | 414 | 415 |
416 | 417 |

We choose to go to the moon

418 | 419 |
422 |

We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard.

423 | 424 |

Because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win, and the others, too.

425 | 426 |

It is for these reasons that I regard the decision last year to shift our efforts in space from low to high gear as among the most important decisions that will be made during my incumbency in the office of the Presidency.

427 | 428 |

In the last 24 hours we have seen facilities now being created for the greatest and most complex exploration in man's history.

429 |
430 |
431 | 432 |
433 | 434 |

We choose to go to the moon

435 | 436 |
440 |

We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard.

441 |

Because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win, and the others, too.

442 |

It is for thesegit reasons that I regard the decision last year to shift our efforts in space from low to high gear as among the most important decisions that will be made during my incumbency in the office of the Presidency.

443 |

In the last 24 hours we have seen facilities now being created for the greatest and most complex exploration in man's history.

444 | 445 |
446 |
447 | 448 |
449 | 450 |

We choose to go to the moon

451 | 452 |
455 |

We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard.

456 |

Because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win, and the others, too.

457 |

It is for these reasons that I regard the decision last year to shift our efforts in space from low to high gear as among the most important decisions that will be made during my incumbency in the office of the Presidency.

458 |

In the last 24 hours we have seen facilities now being created for the greatest and most complex exploration in man's history.

459 |
460 |
461 | 462 |
463 | 464 |

We choose to go to the moon

465 | 466 |
471 |

We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard.

472 |

Because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win, and the others, too.

473 |

It is for these reasons that I regard the decision last year to shift our efforts in space from low to high gear as among the most important decisions that will be made during my incumbency in the office of the Presidency.

474 |

In the last 24 hours we have seen facilities now being created for the greatest and most complex exploration in man's history.

475 |
476 |
477 | 478 |
479 | 480 |

We choose to go to the moon

481 | 482 |
486 |

We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard.

487 |

Because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win, and the others, too.

488 |

It is for these reasons that I regard the decision last year to shift our efforts in space from low to high gear as among the most important decisions that will be made during my incumbency in the office of the Presidency.

489 |

In the last 24 hours we have seen facilities now being created for the greatest and most complex exploration in man's history.

490 |
491 |
492 | 493 |
494 | 495 |

We choose to go to the moon

496 | 497 |
500 |

We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard.

501 |
    502 |
  • That goal will serve to organize and measure
  • 503 |
  • The best of our energies and skill
  • 504 |
  • That challenge is one that we are willing to accept
  • 505 |
  • One we are unwilling to postpone
  • 506 |
  • One which we intend to win
  • 507 |
  • And the others, too
  • 508 |
509 | 510 |

It is for these reasons that I regard the decision last year to shift our efforts in space from low to high gear as among the most important decisions that will be made during my incumbency in the office of the Presidency.

511 |

In the last 24 hours we have seen facilities now being created for the greatest and most complex exploration in man's history.

512 |
513 |
514 | 515 | 516 |
517 | 518 |

We choose to go to the moon

519 | 520 |
524 |

We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy, but because they are hard.

525 |

Because that goal will serve to organize and measure the best of our energies and skills, because that challenge is one that we are willing to accept, one we are unwilling to postpone, and one which we intend to win, and the others, too.

526 |

It is for these reasons that I regard the decision last year to shift our efforts in space from low to high gear as among the most important decisions that will be made during my incumbency in the office of the Presidency.

527 |

In the last 24 hours we have seen facilities now being created for the greatest and most complex exploration in man's history.

528 |
529 |
530 | 531 | 532 |
533 | 534 |

We choose to go to the moon

535 | 536 |
539 |

Less than the defined 400 chars available, so let's do nothing.

540 |
541 |
542 | 543 |
544 | 545 |

Less than the defined 40 words available, so do nadda.

546 | 547 |
551 |

We choose to go to the moon. We choose to go to the moon in this decade and do the other things, not because they are easy.

552 |
553 |
554 |
555 |
556 | 557 |
558 |
559 | 569 | 570 | --------------------------------------------------------------------------------