UNPKG

zuck.js

Version:

A javascript library that lets you add stories EVERYWHERE.

861 lines (710 loc) 24.1 kB
import { DocumentElementWithFullscreen, DocumentWithFullscreen, Maybe, ModalContainer, TimelineItem, ZuckObject } from 'types'; import { findPos, hasWindow, onAnimationEnd, onTransitionEnd, prepend, safeNum } from './utils'; export const modal = (zuck: ZuckObject) => { const id = zuck.id; let modalZuckContainer = document.querySelector<ModalContainer>('#zuck-modal'); if (!modalZuckContainer && !zuck.hasModal) { zuck.hasModal = true; modalZuckContainer = document.createElement('div'); modalZuckContainer.id = 'zuck-modal'; if (zuck.option('cubeEffect')) { modalZuckContainer.className = 'with-cube'; } modalZuckContainer.innerHTML = '<div id="zuck-modal-content"></div>'; modalZuckContainer.style.display = 'none'; modalZuckContainer.setAttribute('tabIndex', '1'); modalZuckContainer.onkeyup = ({ keyCode }) => { const code = keyCode; if (code === 27) { modalZuckContainer.modal.close(); } else if (code === 13 || code === 32) { modalZuckContainer.modal.next(); } }; if (zuck.option('openEffect')) { modalZuckContainer?.classList.add('with-effects'); } if (zuck.option('rtl')) { modalZuckContainer?.classList.add('rtl'); } onTransitionEnd(modalZuckContainer, () => { const modalContent = document.querySelector<HTMLElement>( '#zuck-modal-content' ); if (modalZuckContainer?.classList.contains('closed')) { if (modalContent) { modalContent.innerHTML = ''; } modalZuckContainer.style.display = 'none'; modalZuckContainer.classList.remove('closed'); modalZuckContainer.classList.remove('animated'); } }); document.body.appendChild(modalZuckContainer); } const translate = function ( element?: Maybe<HTMLElement>, to?: number, duration?: number, ease?: string | null ) { if (to === undefined || (to && isNaN(to))) { return; } const direction = to > 0 ? 1 : -1; const modalWidth = document.querySelector<HTMLElement>('#zuck-modal')?.offsetWidth || 1; const to3d = (Math.abs(to) / modalWidth) * 90 * direction; if (zuck.option('cubeEffect')) { const scaling = to3d === 0 ? 'scale(0.95)' : 'scale(0.930,0.930)'; const modalContent = document.querySelector<HTMLElement>( '#zuck-modal-content' ); if (modalContent) { modalContent.style.transform = scaling; } if (to3d < -90 || to3d > 90) { return false; } } const transform = !zuck.option('cubeEffect') ? `translate3d(${to}px, 0, 0)` : `rotateY(${to3d}deg)`; if (element) { if (ease) { element.style.transitionTimingFunction = ease; } element.style.transitionDuration = `${duration}ms`; element.style.transform = transform; } }; const fullScreen = function (elem?: Maybe<HTMLElement>, cancel?: boolean) { const anyDocument = document as DocumentWithFullscreen; const anyElem = elem as DocumentElementWithFullscreen; try { if (cancel) { if ( anyDocument.fullscreenElement || anyDocument.webkitFullscreenElement || anyDocument.mozFullScreenElement || anyDocument.msFullscreenElement ) { if (anyDocument.exitFullscreen) { anyDocument.exitFullscreen().catch(() => {}); } else if (anyDocument.mozCancelFullScreen) { anyDocument.mozCancelFullScreen().catch(() => {}); } } } else { if (anyElem.requestFullscreen) { anyElem.requestFullscreen(); } else if (anyElem.msRequestFullscreen) { anyElem.msRequestFullscreen(); } else if (anyElem.mozRequestFullScreen) { anyElem.mozRequestFullScreen(); } else if (anyElem.webkitRequestFullscreen) { anyElem.webkitRequestFullscreen(); } } } catch (e) { console.warn("[Zuck.js] Can't access fullscreen"); } }; const moveStoryItem = (direction: boolean) => { const modalContainer = document.querySelector<HTMLElement>( '#zuck-modal' ) as ModalContainer; const modalSlider = document.querySelector<HTMLElement>( `#zuck-modal-slider-${id}` ); let target: 'previous' | 'next' | '' = ''; let useless: 'previous' | 'next' | '' = ''; let transform = 0; const slideItems = { previous: document.querySelector<HTMLElement>( '#zuck-modal .story-viewer.previous' ), next: document.querySelector<HTMLElement>( '#zuck-modal .story-viewer.next' ), viewing: document.querySelector<HTMLElement>( '#zuck-modal .story-viewer.viewing' ) }; if ( (!slideItems.previous && !direction) || (!slideItems.next && direction) ) { if (!zuck.option('rtl')) { return false; } } if (!direction) { target = 'previous'; useless = 'next'; } else { target = 'next'; useless = 'previous'; } const transitionTime = 600; if (zuck.option('cubeEffect')) { if (target === 'previous') { transform = safeNum(modalContainer?.slideWidth); } else if (target === 'next') { transform = safeNum(modalContainer?.slideWidth) * -1; } } else { transform = findPos(slideItems[target])[0] * -1; } translate(modalSlider, transform, transitionTime, null); setTimeout(() => { // set page data when transition complete if (zuck.option('rtl')) { const tmp = target; target = useless; useless = tmp; } if (target !== '' && slideItems[target] && useless !== '') { const currentStory = slideItems[target]?.getAttribute('data-story-id'); zuck.internalData.currentStory = currentStory; const oldStory = document.querySelector<HTMLElement>( `#zuck-modal .story-viewer.${useless}` ); if (oldStory) { oldStory?.parentNode?.removeChild(oldStory); } if (slideItems.viewing) { slideItems.viewing?.classList.add('stopped'); slideItems.viewing?.classList.add(useless); slideItems.viewing?.classList.remove('viewing'); } if (slideItems[target]) { slideItems[target]?.classList.remove('stopped'); slideItems[target]?.classList.remove(target); slideItems[target]?.classList.add('viewing'); } const newTimelineItem = getStoryMorningGlory(target); if (newTimelineItem) { createStoryViewer(newTimelineItem, target); } const storyId = zuck.internalData.currentStory; const storyIndex = zuck.findStoryIndex(storyId); const storyWrap = document.querySelector<HTMLElement>( `#zuck-modal [data-story-id="${storyId}"]` ) as HTMLElement; let items: undefined | NodeListOf<Element> = undefined; if (storyWrap) { items = storyWrap.querySelectorAll<HTMLElement>( '[data-index].active' ); const duration = items?.[0]?.firstElementChild as HTMLElement; zuck.data[storyIndex].currentItem = safeNum( items?.[0]?.getAttribute('data-index') ); if (items?.[0]) { items[0].innerHTML = zuck.template('viewerItemPointerProgress')( duration.style.cssText ); onAnimationEnd(duration, () => { zuck.nextItem(); }); } } translate(modalSlider, 0, 0, null); if (items) { const storyViewer = document.querySelector<HTMLElement>( `#zuck-modal .story-viewer[data-story-id="${currentStory}"]` ); zuck.playVideoItem(storyViewer, items); } zuck.callback('onView')(zuck.internalData.currentStory); } }, transitionTime + 50); }; const createStoryViewer = function ( storyData: TimelineItem, className: string, forcePlay?: boolean ) { const modalSlider = document.querySelector<HTMLElement>( `#zuck-modal-slider-${id}` ); const storyItems = storyData['items']; storyData.time = storyItems && storyItems?.[0]['time']; let htmlItems = ''; let pointerItems = ''; const storyId = storyData['id']; const slides = document.createElement('div'); const currentItem = storyData['currentItem'] || 0; const exists = document.querySelector<HTMLElement>( `#zuck-modal .story-viewer[data-story-id="${storyId}"]` ); if (exists) { return false; } slides.className = 'slides'; storyItems.forEach((item, i) => { if (currentItem > i) { storyData.items[i].seen = true; item.seen = true; } pointerItems += zuck.template('viewerItemPointer')(i, currentItem, item); htmlItems += zuck.template('viewerItemBody')(i, currentItem, item); }); slides.innerHTML = htmlItems; const video = slides.querySelector('video'); const addMuted = function (video: HTMLVideoElement) { if (video.muted) { storyViewer?.classList.add('muted'); } else { storyViewer?.classList.remove('muted'); } }; if (video) { video.onwaiting = () => { if (video.paused) { storyViewer?.classList.add('paused'); storyViewer?.classList.add('loading'); } }; video.onplay = () => { addMuted(video); storyViewer?.classList.remove('stopped'); storyViewer?.classList.remove('paused'); storyViewer?.classList.remove('loading'); }; video.onload = video.onplaying = video.oncanplay = () => { addMuted(video); storyViewer?.classList.remove('loading'); }; video.onvolumechange = () => { addMuted(video); }; } const storyViewerWrap = document.createElement('div'); storyViewerWrap.innerHTML = zuck.template('viewerItem')( storyData, storyItems[currentItem] ); const storyViewer = storyViewerWrap.firstElementChild as HTMLElement; const storyViewerPointerWrap = storyViewer.querySelector<HTMLElement>( '.slides-pointers .wrap' ); storyViewer.className = `story-viewer muted ${className} ${ !forcePlay ? 'stopped' : '' } ${zuck.option('backButton') ? 'with-back-button' : ''}`; if (storyId) { storyViewer.setAttribute('data-story-id', storyId); } if (storyViewerPointerWrap) { storyViewerPointerWrap.innerHTML = pointerItems; } storyViewer .querySelectorAll<HTMLDivElement>('.close, .back') .forEach((el) => { el.onclick = (e) => { e.preventDefault(); modalZuckContainer.modal.close(); }; }); storyViewer.appendChild(slides); if (className === 'viewing') { zuck.playVideoItem( storyViewer, storyViewer.querySelectorAll<HTMLElement>( `[data-index="${currentItem}"].active` ), undefined ); } storyViewer .querySelectorAll<HTMLDivElement>( '.slides-pointers [data-index] > .progress' ) .forEach((el) => { onAnimationEnd(el, () => { zuck.nextItem(undefined); }); }); if (!modalSlider) { return; } if (className === 'previous') { prepend(modalSlider, storyViewer); } else { modalSlider.appendChild(storyViewer); } }; const createStoryTouchEvents = function (modalSlider: Maybe<HTMLElement>) { const modalContainer = document.querySelector<ModalContainer>('#zuck-modal'); const enableMouseEvents = true; let position: { x: number; y: number } | null | undefined = null; let touchOffset: | { x: number; y: number; time: number; valid: boolean; } | null | undefined = null; let isScrolling: boolean | null | undefined = null; let delta: { x?: number; y?: number } | null | undefined = null; let timer: ReturnType<typeof setTimeout> | undefined = undefined; let nextTimer: ReturnType<typeof setTimeout> | undefined = undefined; const touchStart = function (event: TouchEvent | MouseEvent) { const storyViewer = document.querySelector<HTMLElement>( '#zuck-modal .viewing' ); const storyViewerWrap = document.querySelector<HTMLElement>( '#zuck-modal .story-viewer' ); if ((event.target as Node).nodeName === 'A') { return; } const touches = (event as TouchEvent).touches ? (event as TouchEvent).touches[0] : (event as MouseEvent); const pos = findPos( document.querySelector<HTMLElement>('#zuck-modal .story-viewer.viewing') ); if (modalContainer) { modalContainer.slideWidth = storyViewerWrap?.offsetWidth; modalContainer.slideHeight = storyViewerWrap?.offsetHeight; } position = { x: pos[0], y: pos[1] }; const clientX = touches.clientX; const clientY = touches.clientY; touchOffset = { x: clientX, y: clientY, time: Date.now(), valid: true }; if (clientY < 80 || clientY > safeNum(modalContainer?.slideHeight) - 80) { touchOffset.valid = false; } else { event.preventDefault(); isScrolling = undefined; delta = {}; if (enableMouseEvents) { modalSlider?.addEventListener('mousemove', touchMove); modalSlider?.addEventListener('mouseup', touchEnd); modalSlider?.addEventListener('mouseleave', touchEnd); } modalSlider?.addEventListener('touchmove', touchMove); modalSlider?.addEventListener('touchend', touchEnd); if (storyViewer) { storyViewer?.classList.add('paused'); } zuck.pauseVideoItem(); timer = setTimeout(() => { if (storyViewer) { storyViewer?.classList.add('longPress'); } }, 600); nextTimer = setTimeout(() => { clearInterval(nextTimer); nextTimer = undefined; }, 250); } }; const touchMove = function (event: TouchEvent | MouseEvent) { const touches = (event as TouchEvent).touches ? (event as TouchEvent).touches[0] : (event as MouseEvent); const clientX = touches.clientX; const clientY = touches.clientY; if (touchOffset && touchOffset.valid) { delta = { x: clientX - touchOffset.x, y: clientY - touchOffset.y }; if (typeof isScrolling === 'undefined') { isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) ); } if (!isScrolling && touchOffset) { event.preventDefault(); translate( modalSlider, safeNum(position?.x) + safeNum(delta?.x), 0, null ); } } }; const touchEnd = (event: TouchEvent | MouseEvent) => { const storyViewer = document.querySelector<HTMLElement>( '#zuck-modal .viewing' ); const lastTouchOffset = touchOffset; const duration = touchOffset ? Date.now() - touchOffset.time : undefined; const isValid = (Number(duration) < 300 && Math.abs(safeNum(delta?.x)) > 25) || Math.abs(safeNum(delta?.x)) > safeNum(modalContainer?.slideWidth) / 3; const direction = safeNum(delta?.x) < 0; const index = direction ? document.querySelector<HTMLElement>('#zuck-modal .story-viewer.next') : document.querySelector<HTMLElement>( '#zuck-modal .story-viewer.previous' ); const isOutOfBounds = (direction && !index) || (!direction && !index); if (touchOffset && !touchOffset.valid) { } else { if (delta) { if (!isScrolling) { if (isValid && !isOutOfBounds) { moveStoryItem(direction); } else { translate(modalSlider, safeNum(position?.x), 300); } } touchOffset = undefined; if (enableMouseEvents) { modalSlider?.removeEventListener('mousemove', touchMove); modalSlider?.removeEventListener('mouseup', touchEnd); modalSlider?.removeEventListener('mouseleave', touchEnd); } modalSlider?.removeEventListener('touchmove', touchMove); modalSlider?.removeEventListener('touchend', touchEnd); } const video = zuck.internalData.currentVideoElement; if (timer) { clearInterval(timer); } if (storyViewer) { zuck.playVideoItem( storyViewer, storyViewer.querySelectorAll<HTMLElement>('.active'), undefined ); storyViewer?.classList.remove('longPress'); storyViewer?.classList.remove('paused'); } if (nextTimer) { clearInterval(nextTimer); nextTimer = undefined; const navigateItem = () => { if (!direction) { if ( safeNum(lastTouchOffset?.x) > document.body.offsetWidth / 3 || !zuck.option('previousTap') ) { if (zuck.option('rtl')) { zuck.navigateItem('previous', event); } else { zuck.navigateItem('next', event); } } else { if (zuck.option('rtl')) { zuck.navigateItem('next', event); } else { zuck.navigateItem('previous', event); } } } }; const storyViewerViewing = document.querySelector<HTMLElement>( '#zuck-modal .viewing' ); if (storyViewerViewing && video) { if (storyViewerViewing?.classList.contains('muted')) { zuck.unmuteVideoItem(video, storyViewerViewing); } else { navigateItem(); } } else { navigateItem(); return false; } } } }; modalSlider?.addEventListener('touchstart', touchStart); if (enableMouseEvents) { modalSlider?.addEventListener('mousedown', touchStart); } }; const getStoryMorningGlory = function (what: 'previous' | 'next' | '') { // my wife told me to stop singing Wonderwall. I SAID MAYBE. const currentStory = zuck.internalData.currentStory; if (currentStory && what !== '') { const element = document.querySelector<HTMLElement>( `#${id} [data-id="${currentStory}"]` ); const foundStory = what === 'previous' ? element.previousElementSibling : element.nextElementSibling; if (foundStory) { const storyId = foundStory.getAttribute('data-id'); const storyIndex = zuck.findStoryIndex(storyId); const data = zuck.data[storyIndex] || false; return data; } } return false; }; const show = (storyId?: TimelineItem['id']) => { const modalContainer = document.querySelector<ModalContainer>('#zuck-modal'); const callback = function () { const modalContent = document.querySelector<HTMLElement>( '#zuck-modal-content' ); modalContent.innerHTML = `<div id="zuck-modal-slider-${id}" class="slider"></div>`; if (!modalContent || !storyId) { return; } const storyIndex = zuck.findStoryIndex(storyId); const storyData = zuck.data[storyIndex]; const currentItem = storyData.currentItem || 0; const modalSlider = document.querySelector<HTMLElement>( `#zuck-modal-slider-${id}` ); createStoryTouchEvents(modalSlider); zuck.internalData.currentStory = storyId; storyData.currentItem = currentItem; if (zuck.option('backNative') && hasWindow()) { window.location.hash = `#!${id}`; } const previousItemData = getStoryMorningGlory('previous'); if (previousItemData) { createStoryViewer(previousItemData, 'previous'); } createStoryViewer(storyData, 'viewing', true); const nextItemData = getStoryMorningGlory('next'); if (nextItemData) { createStoryViewer(nextItemData, 'next'); } if (zuck.option('autoFullScreen')) { modalContainer?.classList.add('fullscreen'); } const tryFullScreen = () => { if ( modalContainer?.classList.contains('fullscreen') && zuck.option('autoFullScreen') && document.body.offsetWidth <= 1024 ) { fullScreen(modalContainer); } modalContainer?.focus(); }; const storyViewerWrap = document.querySelector<HTMLElement>( '#zuck-modal .story-viewer' ); if (zuck.option('openEffect') && modalContainer) { const storyEl = document.querySelector<HTMLElement>( `#${id} [data-id="${storyId}"] .item-preview` ); const pos = findPos(storyEl); modalContainer.style.marginLeft = `${ pos[0] + safeNum(storyEl?.offsetWidth) / 2 }px`; modalContainer.style.marginTop = `${ pos[1] + safeNum(storyEl?.offsetHeight) / 2 }px`; modalContainer.style.display = 'block'; modalContainer.slideWidth = storyViewerWrap?.offsetWidth || 0; setTimeout(() => { modalContainer?.classList.add('animated'); }, 10); setTimeout(() => { tryFullScreen(); }, 300); // because effects } else { if (modalContainer) { modalContainer.style.display = 'block'; modalContainer.slideWidth = storyViewerWrap?.offsetWidth || 0; } tryFullScreen(); } zuck.callback('onView')(storyId); }; zuck.callback('onOpen')(storyId, callback); }; const next = () => { const callback = function () { const lastStory = zuck.internalData.currentStory; const lastStoryIndex = zuck.findStoryIndex(lastStory); const lastStoryTimelineElement = document.querySelector<HTMLElement>( `#${id} [data-id="${lastStory}"]` ); if (lastStoryTimelineElement) { lastStoryTimelineElement?.classList.add('seen'); zuck.data[lastStoryIndex].seen = true; zuck.internalData.seenItems[lastStory] = true; zuck.saveLocalData('seenItems', zuck.internalData.seenItems); zuck.updateStorySeenPosition(); } const stories = document.querySelector<HTMLElement>( '#zuck-modal .story-viewer.next' ); if (!stories) { modalZuckContainer.modal.close(); } else { if (zuck.option('rtl')) { moveStoryItem(false); } else { moveStoryItem(true); } } }; zuck.callback('onEnd')(zuck.internalData.currentStory, callback); }; const close = () => { const modalContainer = document.querySelector<ModalContainer>('#zuck-modal'); const modalContent = document.querySelector<HTMLElement>( '#zuck-modal-content' ); const callback = function () { if (zuck.option('backNative') && hasWindow()) { window.location.hash = ''; } fullScreen(modalContainer, true); if (modalContainer) { if (zuck.option('openEffect')) { modalContainer.classList.add('closed'); } else { if (modalContent) { modalContent.innerHTML = ''; } modalContainer.style.display = 'none'; } } }; zuck.callback('onClose')(zuck.internalData.currentStory, callback); }; modalZuckContainer.modal = { show, next, close }; return modalZuckContainer.modal; };