node-marquee
Advanced tools
Comparing version 2.0.9 to 3.0.5
107
package.json
{ | ||
"name": "node-marquee", | ||
"version": "2.0.9", | ||
"version": "3.0.5", | ||
"description": "Custom Marquee element", | ||
"browserslist": [ | ||
"last 20 versions, > 0.5%, ie >= 10" | ||
], | ||
"scripts": { | ||
"release": "npm publish", | ||
"prepare": "npm version patch && npm run prepare:all && npm run docs && npm run demo-build", | ||
"docs": "typedoc --out docs ./src/ts --excludeNotExported --plugin typedoc-plugin-nojekyll", | ||
"pages-build": "webpack --config ./examples-src/webpack/prod.js", | ||
"pages-dev": "webpack serve --config ./examples-src/webpack/dev.js", | ||
"prepare:all": "npm run prepare:es && npm run prepare:cjs && npm run prepare:cdn", | ||
"prepare:es": "tsc ./src/ts/index.ts --allowJs true --outDir ./dist/es --target es6 --moduleResolution node --declaration true --declarationDir ./dist/types --declarationMap true", | ||
"prepare:cjs": "tsc ./src/ts/index.ts --allowJs true --outDir ./dist/cjs --target es5 --module commonjs", | ||
"prepare:cdn": "NODE_ENV=production webpack --config ./webpack/webpack.cdn.conf.js", | ||
"demo-build": "NODE_ENV=production webpack --config ./webpack/webpack.build.conf.js", | ||
"demo-start": "webpack-dev-server --config ./webpack/webpack.dev.conf.js", | ||
"prepare:es": "tsc ./src/ts/index.ts --outDir ./build/es --target es6 --module es6 --moduleResolution node --esModuleInterop true --allowSyntheticDefaultImports true --declaration true --declarationDir ./build/types --declarationMap true", | ||
"prepare:cjs": "tsc ./src/ts/index.ts --outDir ./build/cjs --target es5 --module commonjs --moduleResolution node --esModuleInterop true --allowSyntheticDefaultImports true", | ||
"prepare:cdn": "webpack --config ./config/webpack.cdn.js", | ||
"docs": "typedoc --out examples-build/docs ./src/ts --theme default --hideGenerator", | ||
"gh-pages": "git add examples-build/docs/* && git commit -m \"Update github pages\" && git subtree push --prefix examples-build/ origin gh-pages", | ||
"lint:js": "eslint . --ext .ts,.js", | ||
"lint:scss": "npx stylelint \"**/*.scss\"", | ||
"precommit": "npm run lint:js && npm run lint:scss" | ||
"prepare": "npm run lint:js && npm run prepare:all && npm run pages-build && npm run docs && npm run gh-pages", | ||
"release": "npm version patch && npm publish" | ||
}, | ||
"main": "dist/cjs/index.js", | ||
"types": "./dist/types/index.d.ts", | ||
"module": "./dist/es/index.js", | ||
"jsdelivr": "dist/cdn/index.js", | ||
"main": "./build/cjs/index.js", | ||
"types": "./build/types/index.d.ts", | ||
"module": "./build/es/index.js", | ||
"jsdelivr": "./build/cdn/index.js", | ||
"sideEffects": false, | ||
"repository": { | ||
@@ -28,3 +31,4 @@ "type": "git", | ||
"keywords": [ | ||
"keyword" | ||
"marquee", | ||
"custom marquee" | ||
], | ||
@@ -34,39 +38,36 @@ "author": "Anthony Bobrov <anton.bobrov@hotmail.com>", | ||
"devDependencies": { | ||
"@babel/cli": "^7.10.5", | ||
"@babel/core": "^7.11.4", | ||
"@typescript-eslint/eslint-plugin": "^3.10.1", | ||
"@typescript-eslint/parser": "^3.10.1", | ||
"autoprefixer": "^9.8.6", | ||
"babel-loader": "^8.1.0", | ||
"clean-webpack-plugin": "^3.0.0", | ||
"copy-webpack-plugin": "^6.0.3", | ||
"css-loader": "^4.2.2", | ||
"css-mqpacker": "^7.0.0", | ||
"cssnano": "^4.1.10", | ||
"eslint": "^7.5.0", | ||
"eslint-config-airbnb": "^18.2.0", | ||
"eslint-plugin-import": "^2.22.0", | ||
"eslint-plugin-jsdoc": "^30.0.0", | ||
"eslint-plugin-jsx-a11y": "^6.3.1", | ||
"eslint-plugin-react": "^7.20.3", | ||
"file-loader": "^6.0.0", | ||
"html-webpack-plugin": "^4.3.0", | ||
"mini-css-extract-plugin": "^0.10.0", | ||
"postcss-loader": "^3.0.0", | ||
"sass": "^1.26.10", | ||
"sass-loader": "^10.0.1", | ||
"source-map": "^0.7.3", | ||
"style-loader": "^1.2.1", | ||
"stylelint": "^13.6.1", | ||
"stylelint-scss": "^3.18.0", | ||
"terser-webpack-plugin": "^4.1.0", | ||
"ts-loader": "^8.0.3", | ||
"typedoc": "^0.18.0", | ||
"typedoc-plugin-nojekyll": "^1.0.1", | ||
"typescript": "^4.0.2", | ||
"webpack": "^4.44.1", | ||
"webpack-cli": "^3.3.12", | ||
"webpack-dev-server": "^3.11.0", | ||
"webpack-merge": "^5.1.2", | ||
"normalize-scss": "^7.0.1" | ||
"@babel/core": "^7.14.6", | ||
"@babel/preset-env": "^7.14.7", | ||
"@babel/preset-typescript": "^7.15.0", | ||
"@types/lodash.mergewith": "^4.6.6", | ||
"@types/normalize-wheel": "^1.0.0", | ||
"@typescript-eslint/eslint-plugin": "^4.28.1", | ||
"@typescript-eslint/parser": "^4.28.1", | ||
"babel-loader": "^8.2.2", | ||
"bootstrap": "^5.0.2", | ||
"clean-webpack-plugin": "^4.0.0-alpha.0", | ||
"copy-webpack-plugin": "^9.0.1", | ||
"css-loader": "^6.2.0", | ||
"cssnano": "^5.0.7", | ||
"eslint": "^7.29.0", | ||
"eslint-config-airbnb-base": "^14.2.1", | ||
"eslint-plugin-import": "^2.23.4", | ||
"eslint-plugin-jsdoc": "^35.4.1", | ||
"html-webpack-plugin": "^5.3.2", | ||
"mini-css-extract-plugin": "^2.1.0", | ||
"node-sass": "^6.0.1", | ||
"normalize-scss": "^7.0.1", | ||
"path": "^0.12.7", | ||
"postcss-loader": "^6.1.1", | ||
"postcss-preset-env": "^6.7.0", | ||
"sass-loader": "^12.1.0", | ||
"style-loader": "^3.2.1", | ||
"terser-webpack-plugin": "^5.1.4", | ||
"ts-loader": "^9.2.3", | ||
"typedoc": "^0.22.7", | ||
"typescript": "^4.3.5", | ||
"webpack": "^5.41.1", | ||
"webpack-cli": "^4.7.2", | ||
"webpack-dev-server": "^3.11.2", | ||
"webpack-merge": "^5.8.0" | ||
}, | ||
@@ -73,0 +74,0 @@ "dependencies": { |
# Custom Marquee element | ||
## Documentation: https://antonbobrov.github.io/node-marquee/ | ||
<br> | ||
### Demo: https://antonbobrov.github.io/node-marquee/ | ||
### Documentation: https://antonbobrov.github.io/node-marquee/docs/ | ||
<br> | ||
## How to start with NPM | ||
@@ -21,3 +28,3 @@ ```sh | ||
```html | ||
<div class="node-marquee">This is a marquee element.</div> | ||
<div id="node-marquee">This is a marquee element.</div> | ||
``` | ||
@@ -29,4 +36,4 @@ ```js | ||
nodeMarquee({ | ||
selector: '.node-marquee' | ||
parent: '#node-marquee' | ||
}); | ||
``` |
@@ -1,2 +0,2 @@ | ||
import { selectOne, isElement, addEventListener } from 'vevet-dom'; | ||
import { selectOne, addEventListener, createElement } from 'vevet-dom'; | ||
import { NodeMarqueeProp, NodeMarquee } from './types'; | ||
@@ -9,64 +9,55 @@ | ||
/** | ||
* Custom Marquee | ||
* Custom Marquee element | ||
*/ | ||
export default function nodeMarquee ( | ||
prop: NodeMarqueeProp = {}, | ||
argProp: NodeMarqueeProp = {}, | ||
): (NodeMarquee | false) { | ||
const className = 'node-marquee'; | ||
let destroyed = false; | ||
// default properties | ||
const DEFAULT_PROP: NodeMarqueeProp = { | ||
selector: '.node-marquee', | ||
const defaults: Required<NodeMarqueeProp> = { | ||
parent: '#node-marquee', | ||
speed: 1, | ||
minQuantity: 4, | ||
autoplay: true, | ||
pauseOnHover: false, | ||
applyOuterStyles: true, | ||
optimizeCalculation: false, | ||
useParentStyles: true, | ||
prependWhitespace: true, | ||
}; | ||
// extend properties | ||
prop = Object.assign(DEFAULT_PROP, prop); | ||
const prop = Object.assign(defaults, argProp); | ||
// states | ||
const className = 'node-marquee'; | ||
// get the outer element | ||
const OUTER = selectOne(prop.selector) as HTMLElement; | ||
// return if the element doesn't exist | ||
if (!isElement(OUTER)) { | ||
// get parent element | ||
const parent = selectOne(prop.parent) as HTMLElement; | ||
if (!(parent instanceof HTMLElement)) { | ||
return false; | ||
} | ||
// add the default class | ||
OUTER.classList.add(className); | ||
parent.classList.add(className); | ||
// states | ||
let isDestroyed = false; | ||
let isPlaying = false; | ||
let progress = 0; | ||
let animationFrame: (false | number) = false; | ||
// get inner text | ||
let text = OUTER.innerHTML; | ||
// quantity of elements | ||
// data | ||
let defaultHTML = parent.innerHTML; | ||
let quantity = 0; | ||
let elements: HTMLElement[] = []; | ||
let elementsWidth: number[] = []; | ||
let items: HTMLElement[] = []; | ||
let itemWidth = 0; | ||
// vars | ||
let translateX = 0; | ||
let isPlaying = false; | ||
// minimum amount of text elements | ||
const MIN_AMOUNT = 4; | ||
// events | ||
let observer: false | MutationObserver = false; | ||
let mutations: undefined | MutationObserver; | ||
const listeners = [ | ||
addEventListener(window, 'resize', create), | ||
addEventListener(parent, 'mouseenter', handleMouseEnter), | ||
addEventListener(parent, 'mouseleave', handleMouseLeave), | ||
]; | ||
// create the marquee | ||
create(); | ||
// create the marquee element | ||
createMarquee(); | ||
// set animation frame | ||
let animationFrame: (false | number) = false; | ||
if (prop.autoplay) { | ||
@@ -78,89 +69,76 @@ play(); | ||
// set events | ||
const RESIZE_LISTENER = addEventListener(window, 'resize', createMarquee.bind(this)); | ||
const MOUSEENTER_LISTENER = addEventListener(OUTER, 'mouseenter', onMouseEnter.bind(this)); | ||
const MOUSELEAVE_LISTENER = addEventListener(OUTER, 'mouseleave', onMouseLeave.bind(this)); | ||
/** | ||
* Create the marquee elemetnt | ||
*/ | ||
function create () { | ||
// reset events | ||
disconnectMutations(); | ||
// Create the marquee element | ||
function createMarquee () { | ||
// disable mutation observer | ||
disconnectMutationsObserver(); | ||
// clear the outer element | ||
quantity = 0; | ||
elements = []; | ||
elementsWidth = []; | ||
OUTER.innerHTML = ''; | ||
items = []; | ||
parent.innerHTML = ''; | ||
// apply styles to the outer | ||
if (prop.applyOuterStyles) { | ||
OUTER.style.position = 'relative'; | ||
OUTER.style.width = '100%'; | ||
OUTER.style.overflow = 'hidden'; | ||
OUTER.style.whiteSpace = 'nowrap'; | ||
if (prop.useParentStyles) { | ||
parent.style.position = 'relative'; | ||
parent.style.width = '100%'; | ||
parent.style.overflow = 'hidden'; | ||
parent.style.whiteSpace = 'nowrap'; | ||
} | ||
// create the first element | ||
const firstEl = createElement(); | ||
let firstElWidth = firstEl.clientWidth; | ||
if (firstElWidth <= 0) { | ||
firstElWidth = window.innerWidth; | ||
// create the first item and get its sizes | ||
// to calculate the further quantity of inner elements | ||
const firstItem = createItem(); | ||
itemWidth = firstItem.clientWidth; | ||
if (itemWidth <= 0) { | ||
itemWidth = window.innerWidth; | ||
} | ||
// calculate how much elements we need to create in addition to the first one | ||
if (firstElWidth < OUTER.clientWidth) { | ||
quantity = Math.ceil(OUTER.clientWidth * 1.5 / firstElWidth); | ||
if (itemWidth < parent.clientWidth) { | ||
quantity = Math.ceil((parent.clientWidth + itemWidth) / itemWidth); | ||
} | ||
if (quantity < MIN_AMOUNT) { | ||
quantity = MIN_AMOUNT; | ||
if (quantity < prop.minQuantity) { | ||
quantity = prop.minQuantity; | ||
} | ||
// and create them | ||
for (let i = 0; i < quantity - 1; i++) { | ||
createElement(true); | ||
// now when we know the total quantity, | ||
// we can create the rest of the items | ||
for (let index = 1; index < quantity; index += 1) { | ||
createItem(true); | ||
} | ||
// update sizes of the elements | ||
updateSizes(); | ||
// render | ||
renderElements(); | ||
// render for the first time | ||
renderItems(); | ||
// enable mutation observer | ||
observeMutations(); | ||
// and to be sure, update sizes once more | ||
if (prop.optimizeCalculation) { | ||
setTimeout(() => { | ||
updateSizes(); | ||
}, 500); | ||
} | ||
setTimeout(() => { | ||
updateSizes(); | ||
}, 500); | ||
} | ||
function createElement ( | ||
absolutePosition = false, | ||
/** | ||
* Create a single element inside the marquee | ||
*/ | ||
function createItem ( | ||
isAbsolute = false, | ||
) { | ||
const element = createElement('div', { | ||
class: `${className}__el`, | ||
html: `${prop.prependWhitespace ? ' ' : ''}${defaultHTML}`, | ||
}); | ||
const el = document.createElement('div'); | ||
el.classList.add(`${className}__el`); | ||
// set text | ||
el.innerHTML = ` ${text}`; | ||
// apply styles | ||
if (absolutePosition) { | ||
el.style.position = 'absolute'; | ||
el.style.top = '0'; | ||
el.style.left = '0'; | ||
if (isAbsolute) { | ||
element.style.position = 'absolute'; | ||
element.style.top = '0'; | ||
element.style.left = '0'; | ||
} | ||
el.style.display = 'inline-block'; | ||
element.style.display = 'inline-block'; | ||
// add the element | ||
OUTER.appendChild(el); | ||
elements.push(el); | ||
// append the element | ||
parent.appendChild(element); | ||
items.push(element); | ||
return el; | ||
return element; | ||
} | ||
@@ -170,4 +148,6 @@ | ||
// when the marquee is hovered | ||
function onMouseEnter () { | ||
/** | ||
* Event on hover - ON | ||
*/ | ||
function handleMouseEnter () { | ||
if (prop.pauseOnHover) { | ||
@@ -178,4 +158,6 @@ pause(); | ||
// when the marquee is not hovered anymore | ||
function onMouseLeave () { | ||
/** | ||
* Event on hover - OFF | ||
*/ | ||
function handleMouseLeave () { | ||
if (prop.pauseOnHover) { | ||
@@ -188,32 +170,48 @@ play(); | ||
// observe changes in DOM | ||
// when there happen some changes, we recreate the marquee element | ||
/** | ||
* Observe DOM content changes | ||
* If a change happend inside the parent element, | ||
* we recreate the marquee element | ||
*/ | ||
function observeMutations () { | ||
// observer config | ||
if (mutations) { | ||
return; | ||
} | ||
const config: MutationObserverInit = { | ||
childList: true, | ||
}; | ||
// oserver callback | ||
const callback: MutationCallback = (mutationsList) => { | ||
for (const mutation of mutationsList) { | ||
if (mutation.type === 'childList') { | ||
text = OUTER.innerHTML; | ||
createMarquee(); | ||
defaultHTML = parent.innerHTML; | ||
create(); | ||
} | ||
} | ||
}; | ||
mutations = new MutationObserver(callback); | ||
mutations.observe(parent, config); | ||
} | ||
// create the observer | ||
observer = new MutationObserver(callback); | ||
observer.observe(OUTER, config); | ||
/** | ||
* Destroy mutation observer | ||
*/ | ||
function disconnectMutations () { | ||
if (mutations) { | ||
mutations.disconnect(); | ||
mutations = undefined; | ||
} | ||
} | ||
function disconnectMutationsObserver () { | ||
if (observer) { | ||
observer.disconnect(); | ||
/** | ||
* Update sizes | ||
*/ | ||
function updateSizes () { | ||
if (isDestroyed) { | ||
return; | ||
} | ||
const width: number[] = []; | ||
for (let index = 0; index < quantity; index += 1) { | ||
width.push(items[index].clientWidth); | ||
} | ||
itemWidth = Math.max(...width); | ||
} | ||
@@ -223,73 +221,46 @@ | ||
// Render the marquee element | ||
/** | ||
* Render the marquee | ||
*/ | ||
function render () { | ||
renderElements(); | ||
if (isPlaying) { | ||
animationFrame = window.requestAnimationFrame(render.bind(this)); | ||
animationFrame = window.requestAnimationFrame(render); | ||
} | ||
renderItems(); | ||
} | ||
// Render the Marquee Elements | ||
function renderElements ( | ||
/** | ||
* Render inner elements | ||
*/ | ||
function renderItems ( | ||
speed = prop.speed, | ||
) { | ||
progress -= speed; | ||
translateX += speed; | ||
let moveToEnd: (HTMLElement | false) = false; | ||
let moveToEndIndex = 0; | ||
// get total width | ||
const totalWidth = itemWidth * (quantity - 1); | ||
let w = 0; | ||
// render elements | ||
for (let i = 0; i < quantity; i++) { | ||
const el = elements[i]; | ||
// get width of the current element | ||
if (!prop.optimizeCalculation) { | ||
elementsWidth[i] = el.clientWidth; | ||
} | ||
const elWidth = elementsWidth[i]; | ||
for (let index = 0; index < quantity; index += 1) { | ||
const el = items[index]; | ||
// calulate transforms | ||
const x = w - translateX; | ||
w += elWidth; | ||
const x = wrap( | ||
-itemWidth, | ||
totalWidth, | ||
progress + itemWidth * index, | ||
); | ||
// apply transforms | ||
el.style.transform = `matrix3d(1,0,0.00,0,0.00,1,0.00,0,0,0,1,0, ${x}, 0, 0,1)`; | ||
if (x < elWidth * -1) { | ||
moveToEnd = el; | ||
moveToEndIndex = i; | ||
} | ||
} | ||
} | ||
if (moveToEnd) { | ||
elements.push(elements.splice(elements.indexOf(moveToEnd), 1)[0]); | ||
translateX -= prop.optimizeCalculation ? elementsWidth[moveToEndIndex] : moveToEnd.clientWidth; | ||
if (prop.optimizeCalculation) { | ||
updateSizes(); | ||
} | ||
} | ||
function wrap ( | ||
min: number, max: number, value: number, | ||
) { | ||
const range = max - min; | ||
return conditionalReturn(value, (val) => ((range + ((val - min) % range)) % range) + min); | ||
} | ||
// Update elements' width | ||
function updateSizes () { | ||
if (destroyed) { | ||
return; | ||
} | ||
for (let i = 0; i < quantity; i++) { | ||
let width = elements[i].clientWidth; | ||
width = elements[i].clientWidth; | ||
if (width <= 0) { | ||
width = window.innerWidth; | ||
} | ||
elementsWidth[i] = width; | ||
} | ||
function conditionalReturn (value: number, func: (val: number) => number) { | ||
return value || value === 0 ? func(value) : func; | ||
} | ||
@@ -299,11 +270,15 @@ | ||
// Start the animation frame | ||
/** | ||
* Play the marquee | ||
*/ | ||
function play () { | ||
if (!animationFrame) { | ||
isPlaying = true; | ||
animationFrame = window.requestAnimationFrame(render.bind(this)); | ||
animationFrame = window.requestAnimationFrame(render); | ||
} | ||
} | ||
// Stop the animation frame | ||
/** | ||
* Pause the marquee | ||
*/ | ||
function pause () { | ||
@@ -319,16 +294,13 @@ isPlaying = false; | ||
// Destroy the marquee | ||
/** | ||
* Destroy the marquee element | ||
*/ | ||
function destroy () { | ||
destroyed = true; | ||
isDestroyed = true; | ||
pause(); | ||
disconnectMutationsObserver(); | ||
RESIZE_LISTENER.remove(); | ||
MOUSEENTER_LISTENER.remove(); | ||
MOUSELEAVE_LISTENER.remove(); | ||
OUTER.innerHTML = text; | ||
disconnectMutations(); | ||
listeners.forEach((listener) => { | ||
listener.remove(); | ||
}); | ||
parent.innerHTML = defaultHTML; | ||
} | ||
@@ -339,11 +311,10 @@ | ||
return { | ||
play: play.bind(this), | ||
pause: pause.bind(this), | ||
play, | ||
pause, | ||
isPlaying: () => isPlaying, | ||
render: renderElements.bind(this), | ||
recreate: createMarquee.bind(this), | ||
updateSizes: updateSizes.bind(this), | ||
destroy: destroy.bind(this), | ||
render: renderItems, | ||
recreate: create, | ||
updateSizes, | ||
destroy, | ||
}; | ||
} |
export interface NodeMarqueeProp { | ||
/** | ||
* The selector of the element or the elements itself. | ||
* @default '.node-marquee' | ||
* @default '#node-marquee' | ||
*/ | ||
selector?: HTMLElement | string; | ||
parent?: HTMLElement | string; | ||
/** | ||
@@ -13,2 +13,7 @@ * The amount of pixels to move with each frame. | ||
/** | ||
* Minimal amount of inner elements | ||
* @default 1 | ||
*/ | ||
minQuantity?: number; | ||
/** | ||
* If you want the marquee element to start moving after its initializing. | ||
@@ -24,17 +29,10 @@ * @default true | ||
/** | ||
* If you need to apply default style to the outer element | ||
* If you need to apply default style to the parent element | ||
* @default true | ||
*/ | ||
applyOuterStyles?: boolean; | ||
useParentStyles?: boolean; | ||
/** | ||
* By default, the script copies the text and moves it. To calculate the transformations, | ||
* it needs to know the width of each element. | ||
* This very width is calculated with each animation frame | ||
* and in some cases may influence performance. | ||
* If you want to avoid this, set the property as "true", and it won't recalculate | ||
* styles with each frame, though when changing styles of the marquee (f.e., font-size), | ||
* the marquee must be manually recreated. | ||
* @default false | ||
* If need to append a whitespace before each element | ||
*/ | ||
optimizeCalculation?: boolean; | ||
prependWhitespace?: boolean; | ||
} | ||
@@ -41,0 +39,0 @@ |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
34
37
36809
15
913