Development Log
ADD link to logo on lock pages
Th following is a tiny piece of code that modifies the size of the logo on password protected pages’ lock screen and makes sure you can click it to go to a link, in my case my home page. Beware that I used specific styling of the lock page in the squarespace editor and this may not work for you as your logo might not be present, or have a different class.
HTML:
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { var images = document.querySelectorAll('.sqs-slice-group img'); images.forEach(function(img) { img.focus(); img.style.cursor = 'pointer'; img.style.setProperty('max-height', '125px', 'important'); img.addEventListener('click', function() { var href = "https://www.polivantage.com"; window.location.href = href; }); }); });
CSS:
Hint image on lock pages
This adds a hint in the form of an image to password protected pages, or in my case, to one individual password protected page. There is some fading involved to not immediately display the hint, as it is part of an obscure mini-quest. Please note that the styling for lock pages in squarespace should happen in the designated code area, not in the CSS menu. If you want to see this in action, please click on the faded text on my homepage.
HTML: <div id="passwordHintContainer" class="passwordHintContainer"> <div id="passwordHint" class="passwordHint"> <div id="passwordKeyboard" class="passwordKeyboard"></div> </div> </div>
JAVASCRIPT: <style> #passwordHintContainer { width: 100%; height: 100%; position: relative; margin: 200px auto; display: flex; box-sizing: border-box; justify-content: center; align-items: center; max-width: 2200px; padding-left: 3vw; padding-right: 3vw; pointer-events: none; } #passwordHint { display: block; width: 600px; height: 198px; margin: 0; padding: 0; pointer-events: auto; cursor: default; background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/1c4e399b-ad6c-43d7-a970-9e29f38a9ea3/PasswordHintKeyboardGraphic.png') no-repeat center center; background-size: contain; z-index: 7777; overflow: hidden; transition: all 3s ease; opacity: 0; } #passwordHint:hover { opacity: 1; transition: all 5s !important; } #passwordKeyboard { display: block; position: relative; width: 100%; height: 100%; margin: 0; padding: 0; pointer-events: auto; cursor: default; background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/277975f7-200f-41b9-8a4b-ed469435681b/PasswordHintAloneGraphic.png') no-repeat center center; background-size: contain; z-index: 8888; overflow: hidden; opacity: 0; transition: all 3s ease; } #passwordKeyboard:hover { opacity: 1; transition: all 23s !important; } .arrow-icon { margin-right: 10px; } .password-input { border-bottom: 2px solid #000 !important; border-image: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 30%, rgba(0,0,0,1) 50%, rgba(0,0,0,1) 70%, rgba(0,0,0,0) 100%); border-image-slice: 1; outline: none !important; padding-left: 10px !important; padding-bottom: 5px !important; } </style> <script> document.addEventListener('DOMContentLoaded', function() { const hintContainer = document.querySelector('.passwordHintContainer'); const passwordBlock = document.querySelector('.sqs-slice-group'); const lockScreen = document.querySelector('.lock-screen'); const subString = 'memberarea'; const getURL = window.location.href; if (getURL.includes(subString)) { passwordBlock.insertAdjacentElement('afterend', hintContainer); } else { hintContainer.remove(); } }); </script>
CSS:
I remind you, do not place the CSS for password protected pages of squarespace into the CSS document as it won’t work. Instead use <style> tags to embed it directly into the designated lock page code section.
Secret pages and hidden shop
I made a mini-quest for the viewer to indulge in. It involves using squarespace’s built-in password protection pages, which sadly doesn’t extend to whole sections of pages, but to individual pages only. The code uses session storage to know whether the user has gained honorary status, modifies pages depending on that status, adds a new menu icon upon activating the honorary status, and is to further be extended in the near future as I add more Easter eggs to the site.
HTML: <div id="sacredBadge" class="sacredBadge" style="display: none;"></div>
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { const subString = 'memberarea'; const subStringTwo = 'hiddenfeatures'; const subStringThree = 'sanctum'; const subStringFour = 'sanctumshop'; const sacredButton = document.getElementById('block-yui_3_17_2_1_1722529618480_18845'); const userAccountLink = document.querySelector('.user-accounts-link'); const userAccountLinkMobile = document.querySelector('.customerAccountLoginMobile'); const getURL = window.location.href; let honoraryStatus = false; let pageVisited = false; const sacredBadge = document.getElementById('sacredBadge'); const newImageUrl = 'https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/8bba2c9d-9a39-4004-bb68-b1a01581a4b8/lanternBrokenGraphic.png'; const headerActions = document.querySelector('.header-actions'); let isTouch = false; function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; } checkHeader(); // Check status and progress if (sessionStorage.getItem('visitedMemberArea')) { pageVisited = true; if (sessionStorage.getItem('sacredButtonClicked') === 'true') { honoraryStatus = true; } else { honoraryStatus = false; } } else { pageVisited = false; } // Store user progress on achieving honorary status if (getURL.includes(subString)) { if (honoraryStatus == false) { sacredButton.addEventListener('click', () => { sessionStorage.setItem('sacredButtonClicked', 'true'); console.log('Honorary Status Activated, my inspecting Adventurer!'); }); sessionStorage.setItem('visitedMemberArea', 'true'); } else { window.location.href = '/hiddenfeatures'; } } // Adjust hidden features page based on status if (getURL.includes(subStringTwo)) { const featuresTextBlock = document.getElementById('block-yui_3_17_2_1_1722529618480_28214'); const featuresTitleBlock = document.getElementById('block-yui_3_17_2_1_1722529618480_25669'); const featuresImageBlock = document.getElementById('block-yui_3_17_2_1_1722529618480_45351'); const featuresText = featuresTextBlock.querySelector('p'); const featuresTitle = featuresTitleBlock.querySelector('h3'); const featuresList = featuresTextBlock.querySelectorAll('ul'); const featuresImages = featuresImageBlock.querySelectorAll('img'); if (honoraryStatus) { // Add logic here for features page if honoraryStatus is true } else { featuresText.textContent = 'You are not supposed to be here Outlander! Be on your way.'; featuresText.style.textAlign = 'center'; featuresList.forEach(ul => ul.remove()); featuresTitle.textContent = 'Features Locked'; featuresImages.forEach(img => { img.src = newImageUrl; img.setAttribute('data-src', newImageUrl); img.setAttribute('data-image', newImageUrl); img.setAttribute('srcset', newImageUrl); }); } } // Display honorary badge link function displayHonoraryBadge() { if (honoraryStatus) { sacredBadge.style.display = 'block'; sacredBadge.addEventListener('click', function() { window.location.href = '/sanctum'; }); if (isTouch === false) { userAccountLink.insertAdjacentElement('afterend', sacredBadge); sacredBadge.classList.remove('mobile'); } else { userAccountLinkMobile.insertAdjacentElement('afterend', sacredBadge); sacredBadge.classList.add('mobile'); } } else { sacredBadge.style.display = 'none'; } } displayHonoraryBadge(); // Adjust sanctum page based on status if (getURL.includes(subStringThree) && !getURL.includes(subStringFour)) { const sanctumTextBlock = document.getElementById('block-yui_3_17_2_1_1722555573944_3982'); const sanctumTitleBlock = document.getElementById('block-yui_3_17_2_1_1722553640132_5223'); const sanctumImageBlock = document.getElementById('block-yui_3_17_2_1_1722555573944_6158'); const sanctumButtonShop = document.getElementById('block-yui_3_17_2_1_1722632329093_9275'); const sanctumText = sanctumTextBlock.querySelector('p'); const sanctumTitle = sanctumTitleBlock.querySelector('h3'); const sanctumImages = sanctumImageBlock.querySelectorAll('img'); if (honoraryStatus) { // Add logic if honoraryStatus is true } else { sanctumTitle.textContent = 'Solitude'; sanctumText.textContent = 'A place of wonders - a place not to be.'; sanctumText.style.textAlign = 'center'; sanctumImages.forEach(img => { img.src = newImageUrl; img.setAttribute('data-src', newImageUrl); img.setAttribute('data-image', newImageUrl); img.setAttribute('srcset', newImageUrl); }); sanctumButtonShop.remove(); // Further modify sanctum when no status here } } });
CSS: //power user menu icon #sacredBadge { width: 55px; height: 55px; margin-left: -15px; margin-right: 7px; pointer-events: auto; background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/c600706e-c07d-4455-8cb5-98f1fd2cc01a/lightningicon.png') no-repeat center center; background-size: contain; z-index: 999; cursor: pointer; transition: 1s; scale: 1; margin-bottom: -3px; } #sacredBadge:hover { scale: 1.25; transition: 0.5s; } #sacredBadge.mobile { width: 100%; height: 90px; margin: 0; }
A lot of the code is based around individual elements which are specific to certain pages. I don’t think it is viable for you to copy and paste this, as the subsequent modification will probably completely restructure the code. I suggest to instead look into session storage and local storage to achieve a similar effect.
Shop category descriptions
I found many solutions on the web to add descriptions to individual shop categories. None appealed to me, so I wrote my own. Currently they fade in nicely, differ in my secret shop, and are fully customizable, including where they appear.
HTML: <div id="categoryDescriptionDiv" class="categoryDescriptionDiv"> <p id="categoryDescription" class="categoryDescription"></p> </div>
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { const catDescDiv = document.getElementById('categoryDescriptionDiv'); const catDesc = document.getElementById('categoryDescription'); const subString = 'shop'; const subStringTwo = 'shop/p/'; const subStringThree = 'sanctumshop'; const getURL = window.location.href; let honoraryStatus = sessionStorage.getItem('sacredButtonClicked') === 'true' ? true : false; const inVeiledShop = getURL.includes(subStringThree); const headerActions = document.querySelector('.header-actions'); let isTouch = false; function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; } checkHeader(); if (getURL.includes(subString) && !getURL.includes(subStringTwo) && isTouch == false) { //assign class names to the description const title = document.getElementsByClassName('nested-category-title')[0]; title.insertAdjacentElement('afterend', catDescDiv); const newClassName = title.textContent.replace(/\s+/g, ''); catDesc.classList.add(newClassName); const classNames = catDesc.classList; // Update text based on class name if (classNames.contains('ArtisticLuxury')) { catDesc.textContent = ''; } else if (classNames.contains('Prints')) { catDesc.textContent = inVeiledShop ? 'Sorcerous sigils are 60x90 cm, 24x36 inches' : 'Wallscapes are 60x90 cm, 24x36 inches'; } else if (classNames.contains('Clothing')) { catDesc.textContent = inVeiledShop ? 'Shrouds and astral attire' : 'You can be wearing these original designs'; } else if (classNames.contains('Accessories')) { catDesc.textContent = inVeiledShop ? 'Ethereal charms and arcane adornments' : 'Items you may need may as well be pretty'; } else if (classNames.contains('Unique')) { catDesc.textContent = inVeiledShop ? 'Legendary artifacts and enigmas' : 'One of a kind'; } else if (classNames.contains('Other')) { catDesc.textContent = inVeiledShop ? 'Omniscient residuary' : 'Gear that is too cool for other categories'; } else { catDesc.textContent = ''; } catDescDiv.classList.add('Active'); } else { catDescDiv.remove(); } });
CSS: .categoryDescriptionDiv { position: relative; display: flex; justify-content: center; align-items: center; text-align: center; opacity: 0; } .categoryDescriptionDiv.Active { animation: fadeIn 5s forwards; } .categoryDescription { width: auto; position: absolute; top:-95px; letter-spacing: 2.1px; margin-left: -11px; font-size: 1.58rem; @media screen and (max-width:790px) { width: auto; position: absolute; top:-50px; letter-spacing: 1.5px; margin-left: -11px; font-size: 1rem; } }
Notice that the main category of “All” has no description assigned to it. I trust in your ability to customize the Fade-in animation in CSS, or to remove it. The text for the descriptions is in JS.
Responsive Vignette
I had trouble with wide-screen monitors displaying my website properly. As part of the solution I decided to add a vignette effect (shadows on the sides). The effect appears only after a certain zoom, or screen/window size. The effect varies depending on pages (secret shop has full vignette, while everything else only has left and right gradients). The result is that on normal monitors there is a tiny bit of a shadowy gradient on the side, on wide-screen monitors it extends from the sides of the screen to where the website content starts, and on mobile it is disabled completely.
HTML: <div id="sideGradientContainer" class="sideGradientContainer"> <div id="sideGradientLeft" class="sideGradientLeft"></div> <div id="sideGradientRight" class="sideGradientRight"></div> </div>
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { const headerActions = document.querySelector('.header-actions'); let isTouch = false; let honoraryStatus = sessionStorage.getItem('sacredButtonClicked') === 'true' ? true : false; function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; } checkHeader(); const sideGradientLeft = document.getElementById('sideGradientLeft'); const sideGradientRight = document.getElementById('sideGradientRight'); const gallery = document.getElementsByClassName("gallery-masonry"); var gradientWidth = 0; const getURL = window.location.href; var contentWidth = 2500; var viewportWidth = window.innerWidth; var viewportHeight = window.innerHeight; //dynamic side gradient width function setWidth() { if (gallery.length <= 0 && viewportWidth > contentWidth) { gradientWidth = (viewportWidth - contentWidth) / 2; } else { gradientWidth = 0; } } setWidth(); // Set the width of the side gradient elements sideGradientLeft.style.width = `${gradientWidth}px`; sideGradientRight.style.width = `${gradientWidth}px`; //Adjust gradients on window resize window.addEventListener('resize', function() { viewportWidth = window.innerWidth; viewportHeight = window.innerHeight; checkHeader(); setWidth(); sideGradientLeft.style.width = `${gradientWidth}px`; sideGradientRight.style.width = `${gradientWidth}px`; }); //add full vignette to the veiled emporium const fullVignette = document.createElement('div'); fullVignette.className = 'fullVignette'; fullVignette.id = 'fullVignette'; //fullVignette.style.opacity = '0'; if (honoraryStatus == true) { const subString = 'sanctumshop'; const getURL = window.location.href; if (getURL.includes(subString)) { document.body.appendChild(fullVignette); } else { fullVignette.remove(); } } // Adjust the full vignette gradient dynamically function adjustVignetteGradient() { if (fullVignette) { const gradientHeight = viewportHeight * 4; const gradientRadius = `${viewportHeight / 1.95}px`; const transitionPosition = viewportHeight / 0.75; const verticalRadius = `${viewportHeight * 1.6}px`; const translateY = (gradientHeight - viewportHeight) / 1.7; const offsetX = 51.5; fullVignette.style.background = `radial-gradient(ellipse at ${offsetX}% center, rgba(0, 0, 0, 0) ${gradientRadius}, rgba(0, 0, 0, 0.4) ${transitionPosition}px, rgba(0, 0, 0, 0.33) ${gradientHeight}px)`; fullVignette.style.height = `${gradientHeight}px`; fullVignette.style.transform = `translateY(-${translateY}px)`; } } adjustVignetteGradient(); window.addEventListener('resize', adjustVignetteGradient); });
CSS: #sideGradientContainer { display: block; position: fixed; height: 100vh; width: 100vw; overflow: hidden; top: 0; left: 0; z-index: 100; pointer-events: none; } #sideGradientLeft { pointer-events: none; height: 100vh; width: 0; left: 0; position: absolute; top: 0; background: linear-gradient(to right, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0) 100%); } #sideGradientRight { pointer-events: none; height: 100vh; width: 0; right: 0; position: absolute; top: 0; background: linear-gradient(to left, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0) 100%); } #fullVignetteContainer { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; display: flex; align-items: center; justify-content: center; z-index: 9999; } #fullVignette { display: block; height: 100vh; width: 100vw; position: fixed; top: 0; left: 0; pointer-events: none; background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0) 70%, rgba(0, 0, 0, 0.5) 100%); z-index: 99999; }
I suggest to only customize the vignette’s opacity in CSS and do the rest in JS. This because the gradient itself and the scaling of the DIVs is handled in JS.
Sorting products
The following is quite a chunk of code, which gives you completely consistent and reliable sorting functionality for your store. The code is concise, there isn’t anything you can leave out without subjugating the functionality. The bulk of the code is reserved for styling the slide-out menu. It started as a drop-down menu and you may see relics of that in the naming conventions. The sorting functionality is in the end of the code, in case you just want that. Currently the code enables a slide-out for mobile, and an always present menu on desktop. The buttons allow sorting of products in any category, by Price, Name, Date, and Randomly. All of these can be reversed when clicking the button again. I am proud of this code, as it saved me from buying an expensive plug-in, and I learned a lot about lists in JS from building it.
HTML: <div id="dropdownMenuContainer" class="dropdownMenuContainer"> <button id="dropdownButton" class="normal dropdownButton"> <div class="dropdown-item-div dropdown-item-div-zero"></div> </button> <div id="dropdownMenu" class="dropdown-content"> <button class="dropdown-item" data-action="time"> <div id="byTime" class="dropdown-item-div dropdown-item-div-one"></div> </button> <button class="dropdown-item" data-action="price"> <div id="byPrice" class="dropdown-item-div dropdown-item-div-two"></div> </button> <button class="dropdown-item" data-action="name"> <div id="byName" class="dropdown-item-div dropdown-item-div-three"></div> </button> <button class="dropdown-item" data-action="random"> <div id="byShuffle" class="dropdown-item-div dropdown-item-div-four"></div> </button> </div> </div>
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { const dropdownButton = document.getElementById('dropdownButton'); const dropdownMenu = document.getElementById('dropdownMenu'); const dropdownMenuContainer = document.getElementById('dropdownMenuContainer'); const dropdownItems = document.querySelectorAll('.dropdown-item'); const subString = 'shop'; const subStringTwo = 'shop/p/'; const getURL = window.location.href; if (getURL.includes(subString) && !getURL.includes(subStringTwo)) { //position the menu var nestedCategories = document.querySelector('.nested-category-tree-wrapper'); nestedCategories.insertBefore(dropdownMenuContainer, nestedCategories.firstChild); dropdownMenuContainer.style.opacity = '1'; let isMenuOpen = false; dropdownButton.classList.remove('open'); dropdownMenu.classList.remove('open'); // Prevent closing when hovering over the dropdown menu dropdownMenu.addEventListener('mouseover', function() { isMenuOpen = true; dropdownButton.classList.add('open'); dropdownMenu.classList.add('open'); }); // Toggle dropdown on main button click dropdownButton.addEventListener('click', function(event) { event.stopPropagation(); // Prevent event from bubbling up to the document if (isMenuOpen) { closeDropdown(); } else { openDropdown(); } }); //Check if in mobile view and open close functionality const headerActions = document.querySelector('.header-actions'); let isTouch = false; function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; if(isTouch === true) { dropdownMenu.style.display = 'none'; dropdownItems.forEach(item => { item.style.display = 'block'; }); dropdownMenu.style.maxHeight = '60px'; dropdownMenu.style.maxWidth = '60px'; isMenuOpen = false; dropdownButton.classList.remove('open'); dropdownMenu.classList.remove('open'); } else { dropdownMenu.style.display = 'block'; dropdownItems.forEach(item => { item.style.display = 'inline-block'; }); dropdownMenu.style.maxWidth = dropdownMenu.scrollWidth + 'px'; dropdownMenu.style.maxHeight = '60px'; isMenuOpen = true; dropdownButton.classList.add('open'); dropdownMenu.classList.add('open'); } } checkHeader(); function openDropdown() { if(isTouch === true) { dropdownMenu.style.display = 'block'; dropdownItems.forEach(item => { item.style.display = 'inline-block'; }); dropdownMenu.style.maxWidth = dropdownMenu.scrollWidth + 'px'; dropdownMenu.style.maxHeight = '60px'; isMenuOpen = true; dropdownButton.classList.add('open'); dropdownMenu.classList.add('open'); } else { dropdownMenu.style.display = 'block'; dropdownItems.forEach(item => { item.style.display = 'inline-block'; }); dropdownMenu.style.maxWidth = dropdownMenu.scrollWidth + 'px'; isMenuOpen = true; dropdownButton.classList.add('open'); dropdownMenu.classList.add('open'); } } function closeDropdown() { if(isTouch === true) { dropdownMenu.style.maxWidth = '0'; isMenuOpen = false; dropdownButton.classList.remove('open'); dropdownMenu.classList.remove('open'); setTimeout(() => { dropdownMenu.style.display = 'none'; }, 300); //ms matches the CSS transition duration } else { dropdownMenu.style.maxWidth = '0'; isMenuOpen = false; dropdownButton.classList.remove('open'); dropdownMenu.classList.remove('open'); setTimeout(() => { dropdownMenu.style.display = 'none'; }, 300); //ms matches the CSS transition duration } } // Add event listeners for individual buttons and for resize window.addEventListener('resize', function(event) { checkHeader(); }); dropdownItems.forEach((button) => { button.addEventListener('click', () => { const action = button.getAttribute('data-action'); handleAction(action); }); }); //For clicks outside the dropdown menu function handleClickOutside(event) { if (!dropdownMenu.contains(event.target) && !dropdownButton.contains(event.target)) { if (isMenuOpen && isTouch === true) { closeDropdown(); } } } //If focus shifting away from the dropdown menu function handleFocusOut(event) { if (!dropdownMenu.contains(event.relatedTarget) && !dropdownButton.contains(event.relatedTarget)) { if (isMenuOpen && isTouch === true) { closeDropdown(); } } } document.addEventListener('click', handleClickOutside); dropdownButton.addEventListener('focusout', handleFocusOut); dropdownMenu.addEventListener('focusout', handleFocusOut); dropdownItems.forEach(item => { item.addEventListener('focusout', handleFocusOut); }); // Handle the sorting actions const productGrid = document.querySelector('.products-flex-container .list-grid'); if (productGrid) { var productItems = Array.from(productGrid.children); const originalOrder = Array.from(productItems); let isPriceAscending = true; let isNameAscending = true; let isOriginalOrder = true; const timeDiv = document.getElementById('byTime'); const priceDiv = document.getElementById('byPrice'); const nameDiv = document.getElementById('byName'); function handleAction(action) { switch(action) { //sort by time added (original and reversed order) case 'time': if (isOriginalOrder) { originalOrder.forEach(item => productGrid.appendChild(item)); timeDiv.classList.add('toggled'); } else { const reversedOrder = [...originalOrder].reverse(); reversedOrder.forEach(item => productGrid.appendChild(item)); timeDiv.classList.remove('toggled'); } isOriginalOrder = !isOriginalOrder; isPriceAscending = true; isNameAscending = true; priceDiv.classList.remove('toggled'); nameDiv.classList.remove('toggled'); break; //sort by price both ways case 'price': productItems.sort((a, b) => { const priceA = parseFloat(a.querySelector('.product-price').textContent.replace('€', '')); const priceB = parseFloat(b.querySelector('.product-price').textContent.replace('€', '')); return isPriceAscending ? priceA - priceB : priceB - priceA; // Toggle sorting }); productItems.forEach(item => productGrid.appendChild(item)); isPriceAscending = !isPriceAscending; priceDiv.classList.toggle('toggled'); isNameAscending = true; isOriginalOrder = true; timeDiv.classList.remove('toggled'); nameDiv.classList.remove('toggled'); break; //sort by name ascending or descending case 'name': productItems.sort((a, b) => { const nameA = a.querySelector('.grid-title').textContent.trim().toLowerCase(); const nameB = b.querySelector('.grid-title').textContent.trim().toLowerCase(); if (nameA < nameB) return isNameAscending ? -1 : 1; if (nameA > nameB) return isNameAscending ? 1 : -1; return 0; }); productItems.forEach(item => productGrid.appendChild(item)); isNameAscending = !isNameAscending; nameDiv.classList.toggle('toggled'); isPriceAscending = true; isOriginalOrder = true; timeDiv.classList.remove('toggled'); priceDiv.classList.remove('toggled'); break; //shuffle randomly case 'random': var productItemsTwo = Array.from(productGrid.children); var originalLength = productItemsTwo.length; for (var i = originalLength; i > 0; i--) { var randomIndex = Math.floor(Math.random() * i); productGrid.appendChild(productItemsTwo[randomIndex]); productItemsTwo.splice(randomIndex, 1); } isPriceAscending = true; isNameAscending = true; isOriginalOrder = true; timeDiv.classList.remove('toggled'); priceDiv.classList.remove('toggled'); nameDiv.classList.remove('toggled'); break; default: } } } } else { dropdownMenuContainer.remove(); } });
CSS: #dropdownMenuContainer { position: relative; width: 0; height: 0; opacity: 0; transition: all 1s ease-out; } #dropdownButton { @media only screen and (min-width:790px) { position:relative; margin-top: 280px; margin-left: -8px; width: 0px; height: 0px; pointer-events: none; } @media only screen and (max-width:790px) { position:relative; margin-top: 334px; margin-left: 84px; width: 60px; height: 60px; pointer-events: auto; } z-index: 9999; padding: 0; cursor: pointer; border: none; background-color: transparent; transition: all 0.3s ease-out; } #dropdownButton.open { @media only screen and (max-width:790px) { margin-left: -40px; } } #dropdownButton:hover { scale: 1.2; } #dropdownMenu.open { @media only screen and (max-width:790px) { margin-left: 25px; } } #dropdownMenu { @media only screen and (min-width:790px) { position:absolute; margin-top: -10px; margin-left: -8px; max-width: 0; white-space: nowrap; cursor: default; } @media only screen and (max-width:790px) { position:absolute; margin-top: -74px; margin-left: 155px; max-height: 0; white-space: nowrap; } padding: 0; display: none; overflow: hidden; transition: all 0.3s ease-out; background-color: transparent; z-index: 222; } .dropdown-item { @media only screen and (max-width:790px) { padding: 5px; width: 60px; height: 60px; margin-bottom: 2px; } @media only screen and (min-width:790px) { padding: 5px; width: 50px; height: 50px; margin-bottom: 0px; } display: block; background-color: transparent; border: none; cursor: pointer; transition: all 0.35s ease-out; } .dropdown-item:hover { transition: all 0.2s ease-out; scale: 1.15; } .dropdown-item-div { display: block; width: 100%; height: 100%; background-size: contain; pointer-events: none; } //main icon .dropdown-item-div-zero { background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/985fb570-4ac0-46e1-8176-2cff784d2eed/cardsIcon1.png') no-repeat center center; background-size: contain; } //date or time icon #byTime { background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/0f257b3b-eba0-4c5d-8884-121b333669d6/TimeForwardIcon.png') no-repeat center center; background-size: contain; } #byTime.toggled { background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/b8c1506e-a711-4980-9b3b-7247ab1cc14e/timeBackwardIcon.png') no-repeat center center; background-size: contain; } //price icon #byPrice { background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/d098c058-9d69-4186-ac0b-ccd85c535480/PriceUpIcon.png') no-repeat center center; background-size: contain; } #byPrice.toggled { background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/bed164b9-ba92-4492-8853-e38fae62a853/PricedownIcon.png') no-repeat center center; background-size: contain; } //name icon #byName { background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/7563f52c-1893-4288-81b7-12f17142f6d5/titleFwdIcon.png') no-repeat center center; background-size: contain; } #byName.toggled { background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/fc6c069d-bd55-4f85-a7b0-91ce3d9d6a46/titleBckIcon.png') no-repeat center center; background-size: contain; } //randomize #byShuffle { background: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/cef3b6dd-acd7-4b2c-8c01-4a6e482c903a/WhirlpoolSpiralIcon.png') no-repeat center center; background-size: contain; animation: reverserotator 30s infinite; }
Notice I again make use of an animation to swirl my randomize button. I trust in your ability to assign your own animation, or not use any at all.
reliable ADDED-TO-CART Pop-over
I was highly unsatisfied with the inbuilt pop-up, or pop-over as it is called, which was supposed to indicate when items were added to cart. It was inconsistent, did not appear on individual product pages at all, was difficult to style, and had no other functionality. I completely disabled it and replaced it with a bar. The bar alerts you in red when the item was not added to cart, is easy to style, upon click takes you to the cart, is extremely consistent, and looks real good. Please note, however, that instead of monitoring cart product quantity, the pop-over appears when the button is clicked, not when the item is actually added to the cart. This makes it snappy, but has its drawbacks.
HTML: <div id="popoverContainer"> <div id="custom-popover"> <p id="popover-message"></p> </div> </div>
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { let hideTimeout; var popover = document.getElementById('custom-popover'); var popoverMessage = document.getElementById('popover-message'); // Add click event listeners to the elements popover.addEventListener('click', function() { window.location.href = '/cart'; }); popoverMessage.addEventListener('click', function() { window.location.href = '/cart'; }); //show and hide popover function showPopover(message) { popoverMessage.textContent = message; popover.classList.add('show'); popover.style.pointerEvents = 'auto'; clearTimeout(hideTimeout); hideTimeout = setTimeout(function() { popover.classList.remove('show'); popover.style.pointerEvents = 'none'; }, 3000); } function checkSelectOptions(productItem) { var selects = productItem.querySelectorAll('select'); for (var i = 0; i < selects.length; i++) { if (selects[i].selectedIndex === 0) { return 'Please Select ' + selects[i].options[0].textContent; } } return null; } function listenForAddToCart() { var buttons = document.querySelectorAll('.sqs-add-to-cart-button'); buttons.forEach(function(button) { button.addEventListener('click', function(event) { let productItem = event.target.closest('.ProductItem') || event.target.closest('.grid-item'); let productName = productItem?.querySelector('.ProductItem-details-title')?.textContent || productItem?.querySelector('.grid-title')?.textContent; if (productItem) { let selectMessage = checkSelectOptions(productItem); if (selectMessage) { popoverMessage.style.color = " #ea4b1a"; showPopover(selectMessage); popover.focus(); return; } } if (productName) { popoverMessage.style.color = "#ffc700"; showPopover(productName + ' Acquired!'); } }); }); } listenForAddToCart(); document.addEventListener('click', function(event) { if (event.target.closest('.sqs-add-to-cart-button')) { listenForAddToCart(); } }); });
CSS: //disable stock pop-overs .template-cart-item-added-popover { display: none; } .sqs-widgets-confirmation-content { display: none; } .sqs-widgets-confirmation-overlay { display: none; } //custom pop-over #popoverContainer { width: 100%; position: relative; margin: 0 auto; display: flex; box-sizing: border-box; justify-content: center; max-width: 2200px; padding-left: 3vw; padding-right: 3vw; pointer-events: none; } #custom-popover { display: block; opacity: 0; position: fixed; transition: all 0.5s ease; color: #ffc700; background: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 15%, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 100%); padding: 25; border: 0; width: 100%; max-width: 2200px; z-index: 999999; align-items: center; text-align: center; font-size: 20px; letter-spacing: 2.5px; pointer-events: none; cursor: pointer; } .popover-message { padding-left: 35px; padding-right: 35px; margin: auto; width: 80%; pointer-events: none; cursor: pointer; } #custom-popover.show { opacity: 1; }
Images Fade-in
The following bit of code allows all images on the website to fade in gradually.
HTML:
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { const images = document.querySelectorAll('img'); images.forEach(function(image) { const observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('loaded'); observer.unobserve(entry.target); } }); }, { root: null, rootMargin: '0px', threshold: 0.2 }); observer.observe(image); }); });
CSS: body:not(.sqs-edit-mode-active) img { opacity: 0; transition: opacity 2s ease-in-out !important; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; } body:not(.sqs-edit-mode-active) img.loaded { opacity: 1; }
Notice that the “body:not(.sqs-edit-mode-active)” tag before the img selector allows the CSS to not be applied in the edit mode of squarespace, allowing you to work on your website in peace, without constantly going back and forth to the CSS to change the opacity value. Otherwise as you enter edit mode, all images become transparent.
Images on add-to-cart buttons
Since the add-to-cart buttons and the options buttons are different from regular buttons, namely their container element is not present, it is difficult to add images to their background. Here is the code which injects DIVs into them, and the CSS for styling the buttons.
HTML:
JAVASCRIPT: document.addEventListener('DOMContentLoaded', () => { const subString = 'shop'; const subStringTwo = 'shop/p/'; const getURL = window.location.href; if (getURL.includes(subString)) { const headerActions = document.querySelector('.header-actions'); let isTouch = false; function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; } checkHeader(); let optionsButtons = document.getElementsByClassName('sqs-view-options-button-wrapper'); const optionsButtonsArray = Array.from(optionsButtons); optionsButtonsArray.forEach(optionButton => { const wrenchDiv = document.createElement('div'); wrenchDiv.classList.add('wrenchDiv'); wrenchDiv.id = "wrenchDiv"; optionButton.appendChild(wrenchDiv); }); let addCartButtons = document.getElementsByClassName('sqs-add-to-cart-button'); const addCartButtonsArray = Array.from(addCartButtons); addCartButtonsArray.forEach(addCartButton => { const addToCartDiv = document.createElement('div'); addToCartDiv.classList.add('addToCartDiv'); addToCartDiv.id = "addToCartDiv"; addCartButton.appendChild(addToCartDiv); if (getURL.includes(subStringTwo) && isTouch === false) { const moveRight = 7.5; const moveUp = -71; addToCartDiv.style.transform = `translate(${moveRight}%, ${moveUp}%)`; } if (getURL.includes(subStringTwo) && isTouch === true) { const moveRightMobile = -1; const moveUpMobile = -75; addToCartDiv.style.transform = `translate(${moveRightMobile}%, ${moveUpMobile}%)`; } }); if (getURL.includes(subStringTwo)) { let addCartButtonsProduct = document.getElementsByClassName('sqs-add-to-cart-button-inner'); const addCartButtonsProductArray = Array.from(addCartButtonsProduct); addCartButtonsProductArray.forEach(addCartButtonProduct => { }); } } });
CSS: .sqs-view-options-button-inner { visibility: hidden; font-size: 0px; } .sqs-view-options-button-inner:after { visibility: visible; font-size: 18px; content: "MODIFY"; letter-spacing: 2.5px; } .sqs-view-options-button-wrapper.sqs-button-element--primary { transition-delay: 0s !important; transition: all 0.8s !important; } .wrenchDiv { height: 50px; width: 100%; display: flex; position: absolute; z-index: 1000; // bottom:5; left:0; pointer-events: none; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/a6276c4e-1a53-4f3b-850e-03091aaca954/wrenchIcon1.png) !important; background-size: contain; background-position: left 5px center; background-clip: content-box !important; background-repeat: no-repeat !important; } //add to cart button picture .addToCartDiv { height: 50px; width: 100%; display: flex; position: absolute; z-index: 1000; // bottom: 1; left:0; pointer-events: none; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/d5c47552-54a2-4d71-99e8-074cd157c8bd/grabdemonIcon1.png) !important; background-size: contain; background-position: left 5px center; background-clip: content-box !important; background-repeat: no-repeat !important; }
There is some more styling required, but you got this.
Dripping ink
You may have noticed the drops of ink running down randomly from top to bottom of the page. These appear sometimes, not always. They are fully scripted, without the use of any images. They have randomized size, speed and direction. They fade out when they are near the bottom of the page.
HTML:
JAVASCRIPT: document.addEventListener('DOMContentLoaded', () => { //are we in mobile view const headerActions = document.querySelector('.header-actions'); let isTouch = false; function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; } checkHeader(); const subString = 'shop'; const subStringTwo = 'legal'; const subStringThree = 'contact'; const subStringFour = 'cart'; const gallery = document.getElementsByClassName("gallery-masonry"); const getURL = window.location.href; if (!getURL.includes(subString) && gallery.length <= 0 && !getURL.includes(subStringTwo) && !getURL.includes(subStringThree) && !getURL.includes(subStringFour)) { const siteWrapper = document.getElementById('siteWrapper'); const container = document.createElement('div'); container.classList.add('dripContainer'); container.id = "dripContainer"; siteWrapper.appendChild(container); function createTrail() { const startX = Math.random() * (window.innerWidth - 10); const startY = -20; let lastX = startX; let lastY = startY; const segmentInterval = isTouch ? 60 : 30; //how often a new segment is spawned in ms const minSize = 12; const maxSize = 17; const size = Math.floor(Math.random() * (maxSize - minSize + 1)) + minSize; const segmentSize = size; const trailInterval = setInterval(() => { const randomOffset = (Math.random() - 0.5) * 3; const newX = lastX + randomOffset; const minSpeed = 0.5; const maxSpeed = 3; const speed = (Math.random() * (maxSpeed - minSpeed + 1)) + minSpeed; const newY = lastY + speed; const pageLength = siteWrapper.scrollHeight; const trailSegment = createTrailSegment(newX, newY, segmentSize); container.appendChild(trailSegment); var pagePercent = (pageLength - lastY) / pageLength; if (pagePercent < 0.2) { trailSegment.style.opacity = Math.min(pagePercent * 5, 1); trailSegment.style.transform = `scale(${Math.min(pagePercent * 5, 1)})`; } setTimeout(() => { trailSegment.style.opacity = 0; trailSegment.style.transform = 'scale(0.3)'; }, 30); trailSegment.addEventListener('transitionend', () => { if (parseFloat(trailSegment.style.opacity) <= 0) { trailSegment.remove(); if (container.children.length === 0) { container.remove(); } } }, { once: true }); lastX = newX; lastY = newY; if (newY >= pageLength - 25) { clearInterval(trailInterval); } }, segmentInterval); } function createTrailSegment(x, y, size) { const trail = document.createElement('div'); const transitionDuration = isTouch ? '4.5s' : '6.5s'; trail.classList.add('trail'); trail.style.left = `${x}px`; trail.style.top = `${y}px`; trail.style.width = `${size}px`; trail.style.height = `${size}px`; trail.style.transition = `opacity ${transitionDuration} ease-out, transform ${transitionDuration} ease-out`; container.style.height = siteWrapper.scrollHeight + 'px'; return trail; } function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function runCreateTrailRandomTimes() { const maxDrips = isTouch ? 1 : 2; const randomNum = getRandomInt(-5, maxDrips); //change the min to lesser number for less frequency of appearing drips let count = 0; function execute() { if (count < randomNum) { createTrail(); count++; const randomDelay = getRandomInt(3, 15) * 1000; setTimeout(execute, randomDelay); } } execute(); } runCreateTrailRandomTimes(); } //forceful cleanup const dripContainer = document.getElementById('dripContainer'); function executeAfterDelay() { if (dripContainer !== null && dripContainer !== undefined) { dripContainer.remove(); } } setTimeout(executeAfterDelay, 69000); });
CSS: #dripContainer { position: absolute; width: 100%; height: 100%; background-color: transparent; pointer-events: none; overflow: hidden; } #dripContainer .trail { position: absolute; background-color: black; border-radius: 50%; width: 10px; height: 10px; filter: blur(1px); z-index: 9999; }
The animation for fading out I leave up to you.
Ink splatter on click
You may have noticed that when clicking on an interactive element such as a link, or a button, a splatter of ink appears. The splatter also appears when clicking anywhere on the home page, with additional splatter on drag functionality. Notice that I specify which elements are allowed to have a splatter effect, this was necessary because initially the splatter was inconsistent, as buttons, links and DIVs may have different classes and pointer events assigned to them. Also in the JS code I found no better way to check whether I am on the homepage, than to check for the presence of a specific link.
HTML:
JAVASCRIPT: document.addEventListener('DOMContentLoaded', () => { var radius = 30; var maxRadius = Math.random() * 15 + radius; var splatterCount = Math.random() * 20 + 10; var duration = Math.random() * 2 + 3; let isDragging = false; let Drag = false; function createSplatter(x, y) { if (Drag == true) { radius = 10; } else { radius = 30; } for (let i = 0; i < splatterCount; i++) { const angle = Math.random() * 2 * Math.PI; const radius = Math.random() * maxRadius; const offsetX = radius * Math.cos(angle); const offsetY = radius * Math.sin(angle); createCircle(x + offsetX, y + offsetY, Math.random() * 5 + 2); } } function createCircle(x, y, radius) { const circle = document.createElement('div'); circle.classList.add('splatter'); circle.style.width = `${radius * 2}px`; circle.style.height = `${radius * 2}px`; circle.style.left = `${x - radius}px`; circle.style.top = `${y - radius}px`; circle.style.backgroundColor = '#000000'; circle.style.position = 'absolute'; circle.style.zIndex = '1000'; // Randomize the fade duration circle.style.animation = `splatterFade ${duration}s forwards`; document.body.appendChild(circle); // Remove the circle after the animation setTimeout(() => { if (circle.parentElement) { circle.parentElement.removeChild(circle); } }, 5500); } const linkToCheck = 'https://www.polivantage.com/memberarea'; const linkExists = Array.from(document.querySelectorAll('a')) .some(link => link.href === linkToCheck); if (linkExists) { isDragging = false; Drag = false; document.body.addEventListener('mousedown', (event) => { isDragging = true; Drag = false createSplatter(event.pageX, event.pageY); }); document.body.addEventListener('mousemove', (event) => { if (isDragging) { Drag = true createSplatter(event.pageX, event.pageY); } }); document.body.addEventListener('mouseup', () => { isDragging = false; Drag = false }); } else { document.body.addEventListener('mousedown', (event) => { const clickedElement = event.target; //classes that provoke splatter const clickableClasses = ['button', 'btn', 'link', 'icon', 'show-pigeon', 'dropdown-item', 'search-input', 'category-item', 'sqs-add-to-cart-button', 'variant-select-wrapper', 'ProductItem-gallery-thumbnails-item-image', 'sqs-block-button-container', 'auth', 'Cart-inner', 'header-title-logo', 'accordion-item__click-target', 'accordion-item__title', 'accordion-icon-container', 'play', 'pause', 'trackBullet', 'trail', 'cart-row-remove', 'sacredBadge']; //tags that provoke splatter const isClickable = clickableClasses.some(cls => clickedElement.classList.contains(cls)); if (isClickable || clickedElement.tagName === 'A' || clickedElement.tagName === 'path' || clickedElement.tagName === 'SELECT' || clickedElement.tagName === 'TEXTAREA' || clickedElement.tagName === 'INPUT') { isDragging = true; Drag = false splatterCount = Math.random() * 7 + 5; duration = Math.random() * 2 + 1; radius = 15; maxRadius = Math.random() * 17 + radius; createSplatter(event.pageX, event.pageY); } }); } });
CSS: .splatter { position: absolute; z-index: 9999; border-radius: 50%; //width: 100px; //height: 100px; display: block; background-color: #000000; filter: blur(0.1px); pointer-events: none; animation: splatterFade 5s forwards; }
The animation for fading out I leave up to you.
Audio playER customization
The inbuilt audio player of squarespace is good. But it doesn’t allow for much customization. I went above and beyond to extend the theme of bullet points to the audio player interface. To start I used the minimalist audio player elements, subsequently adding my own elements. Now as you can see on my audio page, there is a bullet at the start. The bullet starts moving as you click on it to play the track. As the bullet reaches the end it explodes. Also the bullet eats the title of the track, which re-appears as it passes it. Additionally the play/pause buttons are customized to resemble a stopwatch, to give the appeal of controlling time.
HTML:
JAVASCRIPT: document.addEventListener("DOMContentLoaded", function() { const subString = 'polivantage.com/sound'; const getURL = window.location.href; if (getURL.includes(subString)){ window.addEventListener('load', function() { let allTracks = document.getElementsByClassName('audio-block'); for (let i = 0; i < allTracks.length; i++) { //find the track position element const trackPosition = allTracks[i].querySelector('.played'); //create bullet and append it to track position element let trackBullet = document.createElement('div'); trackBullet.classList.add('trackBullet'); trackPosition.appendChild(trackBullet); //set track position to the beginning trackPosition.style.width = '0%'; //on play and on pause const actions = allTracks[i].querySelector('.action'); const title = allTracks[i].querySelector('.title-wrapper'); const track = allTracks[i].querySelector('.track'); const widthTitle = title.clientWidth; const widthTrack = track.clientWidth; const widthBullet = trackBullet.clientWidth; const titleWidthPercent = (widthTitle / widthTrack) * 100; const bulletWidthPercent = (widthBullet / widthTrack) * 100; actions.style.opacity = '0'; trackPosition.style.setProperty('background-color', 'FFC700', 'important'); const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.attributeName === 'style') { const newWidth = parseFloat(trackPosition.style.width); if (newWidth > 6 && newWidth < 98) { if (newWidth >= titleWidthPercent+bulletWidthPercent) { trackPosition.style.setProperty('background-color', 'transparent', 'important'); } else { trackPosition.style.setProperty('background-color', '#FFC700', 'important'); } actions.style.opacity = '1'; trackBullet.classList.add('trackBullet'); } else { actions.style.opacity = '0'; trackBullet.classList.add('trackBullet'); trackPosition.style.setProperty('background-color', '#FFC700', 'important'); } if (newWidth >= 98) { actions.style.opacity = '1'; trackBullet.classList.add('bulletCrash'); trackBullet.classList.remove('trackBullet'); } } }); }); observer.observe(trackPosition, { attributes: true, attributeFilter: ['style'] }); } }); } });
CSS: .bulletCrash { width: 60px; height: 60px; display: block; position: absolute; top: 0; right: 0; background-size: contain; background-position: center; background-repeat: no-repeat; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/0052902b-b84e-482f-bfce-7a31cda4532d/crashExplosionIcon1.png); } .trackBullet { width: 60px; height: 60px; display: block; position: absolute; top: 0; right: -22px; background-size: contain; background-position: center; background-repeat: no-repeat; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/31b85fc9-5f5f-469d-8e71-8d50545821f4/BulletPointRightIcon1.png); } .audio-block .played { background-color: #ffc700 !important; transition: all 0.5s ease; left: 0 !important; width: 100%; height: 100%; justify-content: right; } .audio-block .player { justify-content: left; display: flex; position: relative; } .sqs-widgets-audio-player-content { background: #ffc700; // border: 0px solid black; border-bottom: 2px solid; border-image: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 20%, rgba(0,0,0,1) 50%, rgba(0,0,0,1) 80%, rgba(0,0,0,0) 100%); border-image-slice: 1; } .audio-block .artistName { display: none; } .audio-block .track { background: transparent; } .audio-block .action { align-items: center; background-size: contain; background-position: center; background-repeat: no-repeat; padding-right: 2%; padding-left: 2%; margin-bottom: 10px !important; justify-content: center; border: 0px; transition: opacity 0.5s ease; opacity: 0; left: 22px; } .audio-block .action:hover { background: transparent !important; } .play { background-size: contain; background-position: center; background-repeat: no-repeat; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/6cde2b46-9de3-4f5b-9f7a-85acebe1bf0b/stopwatchTwoIcon1+copy.png); width: 100%; height: 100%; position: absolute; top:0; left: 0%; @media only screen and (max-width:790px) { opacity: 0 !important; } } .audio-block .play-button { width: 100%; height: 100%; opacity: 0; position: absolute; top:0%; left: 0%; } .pause { width: 100% !important; height: 100% !important; position: absolute !important; top:0; left: 0; border: 0px !important; background-size: contain; background-position: center; background-repeat: no-repeat; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/6ca8a9da-04ae-4eb0-beab-642e3258e05a/stopwatchOneIcon1+copy.png); } .audio-block .icon { display: none; } .audio-block .secondary-controls { display: flex; align-items: center; } .audio-block .title { letter-spacing: 0.1rem; font-size: 1.2rem !important; line-height: 1.5rem; color: #000000; padding-left: 30px; transition: opacity 0.5s ease; @media only screen and (max-width:790px) { padding-left: 50px; } } .title-wrapper { padding-top: 4px; } .audio-block .time { display: none; } .bullet { background-size: contain; background-position: center; background-repeat: no-repeat; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/31b85fc9-5f5f-469d-8e71-8d50545821f4/BulletPointRightIcon1.png); height: auto; width: 50px }
Replacing Bullet points with real bullets
This one does two things. Firstly the CSS makes sure that all unnumbered lists which usually begin with a dot ("•"), have instead an icon of a bullet in front of them. Secondly it finds all characters of a list dot, and restructures the text into a list. This was necessary because the CSS alone works fine for when I actually write a list. However, when adding product information through a third-party implementation, the lists are converted to just paragraphs with the dot ("•") characters inside them. Now whenever there is a list dot on the product page, it automatically restructures the text to insert bullet icons instead.
HTML:
JAVASCRIPT: document.addEventListener("DOMContentLoaded", function() { //Bullet points const subString = 'shop'; const getURL = window.location.href; if (getURL.includes(subString)){ const textNodes = []; function getTextNodes(node) { if (node.nodeType === Node.TEXT_NODE) { textNodes.push(node); } else { node.childNodes.forEach(getTextNodes); } } getTextNodes(document.body); textNodes.forEach(node => { const text = node.nodeValue; if (text.includes("•")) { const items = text.split("•").map(item => item.trim()).filter(item => item.length > 0); if (items.length > 0) { const ul = document.createElement("ul"); items.forEach(item => { const li = document.createElement("li"); const paragraph = document.createElement('p'); paragraph.textContent = item; paragraph.classList.add('preFade', 'fadeIn', 'p-with-before'); paragraph.style.whiteSpace = 'pre-wrap'; li.appendChild(paragraph); ul.appendChild(li); }); node.parentNode.insertBefore(ul, node); node.parentNode.removeChild(node); let nextSibling = ul.nextSibling; while (nextSibling) { if (nextSibling.nodeType === Node.ELEMENT_NODE && nextSibling.tagName === 'BR') { nextSibling.parentNode.removeChild(nextSibling); } nextSibling = nextSibling.nextSibling; } } } }); } //Links no focus const links = document.querySelectorAll('a'); links.forEach(link => { //link.setAttribute('tabindex', '-1'); link.addEventListener('focus', (event) => { event.target.blur(); }); }); });
CSS: ul { list-style: none; padding-left: 0; } ul:not(.accordion-items-container) > li > p::before { content: " " !important; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/31b85fc9-5f5f-469d-8e71-8d50545821f4/BulletPointRightIcon1.png) !important; background-size: contain; background-position: center top 6px ; background-repeat: no-repeat; margin-right: 20px !important; margin-left: 0px !important; margin-top: 0rem; margin-bottom: 0.5rem; padding-bottom: 0px; width:40px !important; height: 40px !important; display: inline-block; } .bullet { background-size: contain; background-position: center; background-repeat: no-repeat; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/31b85fc9-5f5f-469d-8e71-8d50545821f4/BulletPointRightIcon1.png); height: auto; width: 50px }
Variant Select Animated Gear Icon
Browsing my shop you may have noticed that whenever you get to select a product variant, such as color, or size, you have a rotating cog before the menu. Aethetically amazing, I love this little extra. Here is how I achieved it:
HTML:
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { function addGears(){ const subString = 'shop'; const subStringTwo = 'shop/p/'; const getURL = window.location.href; if (getURL.includes(subString)){ let allSelectWrappers = document.querySelectorAll('.variant-select-wrapper'); allSelectWrappers.forEach(function(wrapper) { const gearDiv = document.createElement('div'); gearDiv.id = "spinner"; gearDiv.classList.add('option-select-gear', 'spinner'); wrapper.appendChild(gearDiv); if (getURL.includes(subStringTwo)){ gearDiv.style.top = '30%'; } else if (getURL.includes(subString)) { gearDiv.style.top = '17%'; } else { } }); } } addGears(); });
CSS: .option-select-gear { background-image: url('https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/3b177fcd-72b2-462f-a563-e73fd964de8f/Gearicon1.png'); border: 0px !important; width: 25px !important; height: 25px !important; left: -35px; position: absolute; pointer-events: none; } .spinner { background-repeat: no-repeat; background-position: center; background-size: contain; animation: rotator 33s linear infinite; }
You may notice in the Javascript that I use some styling to move the cogs around depending on whether we are in the individual product page, or main shop category view. This was necessary for proper alignment.
Product image carousel in shop categories
The individual product pages have nifty arrows on the side of the image preview to scroll through the images of a product, these are called “carousel”. I wanted that functionality extended to the category view - to be able to quickly scroll product images without entering the individual product page.
HTML:
JAVASCRIPT: <script> document.addEventListener('DOMContentLoaded', function() { const subString = 'shop'; const subStringTwo = 'shop/p/'; const getURL = window.location.href; if (getURL.includes(subString) && !getURL.includes(subStringTwo)) { const headerActions = document.querySelector('.header-actions'); let isTouch = false; function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; } checkHeader(); function cloneImageArrows() { const products = document.querySelectorAll('.product-lists-item'); products.forEach((product, index) => { const allImages = product.querySelectorAll('.grid-item-image'); if (allImages.length >= 2) { const clonedElement = document.createElement('div'); const productImage = product.querySelector('.grid-image'); productImage.prepend(clonedElement); clonedElement.id = `carousel${index + 1}`; clonedElement.classList.add('carousel', 'categoryArrows', 'ProductItem-gallery-carousel-controls'); const leftArrow = document.createElement('div'); const rightArrow = document.createElement('div'); clonedElement.appendChild(leftArrow); clonedElement.appendChild(rightArrow); leftArrow.classList.add('arrowButton', 'arrowPrev', 'ProductItem-gallery-prev', 'product-item-gallery-carousel-control'); rightArrow.classList.add('arrowButton', 'arrowNext', 'ProductItem-gallery-next', 'product-item-gallery-carousel-control'); function adjustImageArrows() { if (!isTouch) { leftArrow.style.position = 'absolute'; leftArrow.style.left = '-14%'; leftArrow.style.transform = 'scale(0.33)'; rightArrow.style.position = 'absolute'; rightArrow.style.right = '-14%'; rightArrow.style.transform = 'scale(0.33)'; } else { leftArrow.style.position = 'absolute'; leftArrow.style.left = '0%'; leftArrow.style.transform = 'scale(1.2)'; rightArrow.style.position = 'absolute'; rightArrow.style.right = '0%'; rightArrow.style.transform = 'scale(1.2)'; } } adjustImageArrows(); allImages.forEach((image, i) => { if (i !== 0) image.style.display = 'none'; }); productImage.addEventListener('click', function(event) { const clickedArrow = event.target.closest('.arrowButton'); if (!clickedArrow) return; event.preventDefault(); const direction = clickedArrow.classList.contains('arrowPrev') ? 'left' : 'right'; handleArrowClick(direction, allImages); }); function handleArrowClick(direction, images) { const imagesArray = Array.from(images); const activeImage = imagesArray.find(image => image.style.display === 'block'); const currentIndex = imagesArray.indexOf(activeImage); let newIndex; if (direction === 'left') { newIndex = currentIndex > 0 ? currentIndex - 1 : imagesArray.length - 1; } else { newIndex = currentIndex < imagesArray.length - 1 ? currentIndex + 1 : 0; } activeImage.style.display = 'none'; activeImage.classList.remove('active'); imagesArray[newIndex].classList.add('active'); imagesArray[newIndex].style.display = 'block'; imagesArray[newIndex].classList.remove('grid-image-hover'); } window.addEventListener('resize', function() { const prevIsTouch = isTouch; checkHeader(); if (isTouch !== prevIsTouch) { adjustImageArrows(); } }); } }); } cloneImageArrows(); } }); </script>
CSS: //PRODUCT CATEGORY CAROUSEL// .product-item-gallery-carousel-control::after { font-weight: bold; border-right: 3px solid black !important; border-top: 3px solid black !important; border-image: linear-gradient(45deg, rgba(0,0,0,0) 50%, rgba(0,0,0,1) 100%); border-image-slice: 1; background-size: contain; background-position: center; background-repeat: no-repeat; transition: all 1s ease; } .product-item-gallery-carousel-control:hover::after { background: linear-gradient(45deg, rgba(0,0,0,0) 50%, rgba(255,199,0,0.8) 100%,); } .carousel { z-index: 9999; position: absolute; width: 100%; height: 100%; display: flex; align-items: center; } .grid-item-image { color: transparent; opacity: 0% !important; transition: all 1s ease-in-out !important; } .grid-item-image.active { opacity: 100% !important; display: block; }
Resume And Continue Shopping Buttons
There are some semi-expensive plug-ins to add the “resume shopping” button to your cart page. As a personal exercise I opted for creating my own. Along with it I modified the “continue shopping” button. The beauty of my implementation is that these buttons now not only take you back to the last browsed category of the shop, no matter whether you have visited other pages in-between. They also take you to the product you viewed last. This functionality is sadly not available on mobile devices as of this writing. I believe it has to do with scroll tags (#) support, or how scrolling is handled on mobile. Nevertheless, I am proud of this solution as it seems even more robust than the commercial plug-ins. On mobile it simply takes you back to the shop category, not the product.
HTML:
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { var screenWidth = window.innerWidth; const subString = 'shop'; const subStringTwo = 'shop/p/'; let lastScrollPosition = window.scrollY; let getURL = window.location.href; getURL = getURL.split('#')[0]; // Purify URL from any position if (getURL.includes(subString) && !getURL.includes(subStringTwo)) { window.addEventListener('scroll', function() { const currentScrollPosition = window.scrollY; if (Math.abs(currentScrollPosition - lastScrollPosition) >= 100) { lastScrollPosition = currentScrollPosition; const viewportHeight = window.innerHeight; const viewportTop = window.scrollY; const viewportBottom = viewportTop + viewportHeight; const viewportMiddle = viewportTop + viewportHeight / 2; let products = document.querySelectorAll('.post-type-store-item'); products.forEach((element, index) => { var rect = element.getBoundingClientRect(); const elementTop = rect.top + window.scrollY; const elementBottom = elementTop + rect.height; const elementMiddle = elementTop + rect.height / 2; // Check if the element's middle is within the viewport middle if (elementMiddle >= viewportTop && elementMiddle <= viewportBottom) { const elementId = element.id; sessionStorage.setItem('scrollPosition', elementId); sessionStorage.setItem('shoppingURL', getURL); } }); } }); } // Monitor cart container and add button var cartString = 'cart'; if (getURL.includes(cartString)) { function observeCartContainer() { // Create a new observer instance const observer = new MutationObserver((mutationsList) => { // Check each mutation for (let mutation of mutationsList) { // Check added children divs for cart container if (mutation.type === 'childList') { const addedNodes = Array.from(mutation.addedNodes); addedNodes.forEach(node => { if (node.nodeType === 1 && node.classList.contains('cart-container')) { // Remove existing variants of the button const existingShopBtns = document.querySelectorAll('.resumeShoppingButton'); if (existingShopBtns.length > 0) { existingShopBtns.forEach(element => { element.remove(); }); } // Create the resume button const resumeShoppingButton = document.createElement('button'); resumeShoppingButton.id = "resumeShoppingButton"; resumeShoppingButton.classList.add('resumeShoppingButton', 'sqs-block-button-container', 'sqs-button-element--primary'); resumeShoppingButton.type = 'button'; resumeShoppingButton.textContent = 'Resume Browsing'; // Append the button const lastChild = document.querySelector('.cart-container > :last-child'); lastChild.insertAdjacentElement('afterend', resumeShoppingButton); // Assign the button URL resumeShoppingButton.addEventListener('click', function() { var scrollPosition = sessionStorage.getItem('scrollPosition'); var shoppingUrl = sessionStorage.getItem('shoppingURL'); if (!shoppingUrl || screenWidth <= 790) { shoppingUrl = 'https://www.polivantage.com/shop'; scrollPosition = 0; } var resumeShopUrl = `${shoppingUrl}#${scrollPosition}`; window.location.href = resumeShopUrl; }); } }); } // Check removed text children for cart container if (mutation.removedNodes.length > 0) { mutation.removedNodes.forEach(node => { if (node.nodeType === 3) { const lastChild = document.querySelector('.cart-container > :last-child'); if (lastChild !== null) { // Make sure the button is last child lastChild.insertAdjacentElement('afterend', resumeShoppingButton); } } }); } var continueShoppingButton = document.querySelector('.cart-continue-button'); if (continueShoppingButton) { var scrollPosition = sessionStorage.getItem('scrollPosition'); var shoppingUrl = sessionStorage.getItem('shoppingURL'); if (!shoppingUrl || screenWidth < 790) { shoppingUrl = 'https://www.polivantage.com/shop'; scrollPosition = 0; } var resumeShopUrl = `${shoppingUrl}#${scrollPosition}`; continueShoppingButton.href = resumeShopUrl; } } }); observer.observe(document.body, { childList: true, subtree: true }); } observeCartContainer(); } });
CSS: //RESUME SHOPPING BUTTON// .resumeShoppingButton { -webkit-tap-highlight-color: transparent; -moz-tap-highlight-color: transparent; display: block; cursor: pointer; // padding-left: 70px !important; padding-right: 15px !important; padding-top: 15px !important; padding-bottom: 27px !important; margin-top: 60px; margin-bottom: 20px; min-width: 335px; height: 50px; color: #000000; letter-spacing: 2.5px !important; font-size: 20px; text-align: right !important; text-transform : uppercase; // text-align: right !important; align-items: center !important; position: relative; @media only screen and (min-width:790px) { transform: translateX(0%) translateY(0%) !important; } @media only screen and (max-width:790px) { width: 100%; padding-bottom: 38px !important; padding-left: 44% !important; margin-bottom: 40px; } } .resumeShoppingButton:hover { @media only screen and (min-width:790px) { color: #FFC700 !important; } @media only screen and (max-width:790px) { color: #FFC700 !important; } } @media only screen and (max-width:790px) { .resumeShoppingButton:focus { background-color: #FFC700 !important; color: #000000 !important; } .resumeShoppingButton:active { background-color: #FFC700 !important; color: #000000 !important; } } //CONTINUE SHOPPING BUTTON// a.cart-continue-button { color: #000000 !important; font-size: 22px !important; text-transform: uppercase !important; letter-spacing: 4px !important; padding-right: 10px !important; } a.cart-continue-button:hover { color: #FFC700 !important; } @media only screen and (max-width: 790px) { a.cart-continue-button { margin: 0 auto !important; left: 50% !important; transform: translateX(-50%); position: absolute; width: 83% !important; } }
There is more CSS tucked away in general button make-up which isn’t presented here. I trust in your ability to style your own buttons.
INSET BOX Shadows on images
It is currently not possible to add box shadows to images directly, only to DIVs which are essentially container elements. This meant that I was unable to use pure CSS to add the desired effect to my art gallery images and product previews in the shop. The solution was to dynamically create an overlaying DIV for every image and apply border shadows to it. Here is the code (with Jquery):
HTML:
JAVASCRIPT: <!--OVERLAY SHADOWS ON SHOP PRODUCT IMAGES --> <script> document.addEventListener('DOMContentLoaded', function() { if(document.querySelector('.products-flex-container')){ var gallery = document.querySelector('.products-flex-container'); let images = gallery.querySelectorAll('figure'); for (let element of images) { var overlayshadow = document.createElement("div"); overlayshadow.id = 'overlayshadow'; overlayshadow.className = 'overlayshadow'; $(overlayshadow).appendTo(element); } } else { } }); </script> <!--OVERLAY SHADOWS ON MASONRY GALLERY IMAGES--> <script> document.addEventListener('DOMContentLoaded', function() { if(document.querySelector('.gallery-masonry-item-wrapper')) { var gallery = document.querySelector('.gallery-masonry'); let images = gallery.querySelectorAll('figure'); for (let element of images) { var overlayshadow = document.createElement("div"); overlayshadow.id = 'overlayshadow'; overlayshadow.className = 'overlayshadow'; $(overlayshadow).appendTo(element); } } else { } }); </script>
CSS: .overlayshadow { height: 100%; width: 100%; display: flex; position: absolute; box-shadow: inset 0px 0px 7px -1px rgba(0,0,0,0.75); z-index: 999999; top:0; bottom:0; left:0; right:0; pointer-events: none; } .gallery-lightbox-item .overlayshadow { display: none; }
Notice how the gallery lightbox preview is omitted from having shadows.
Adding Stock search in shop (Redundant)
Please note that this post has been made obsolete by finding better ways of implementing search into various parts of the website. This post will remain here for archival purposes. That said, I found it alarmingly difficult to implement a search function for my website using only the Squarespace tools. I found out that currently the search only works for the store and the blog, not other content. As per instructions from the web, I settled on creating a search bar element in my footer, and moving it to my store category menu with code. Thus the referenced search bar is a Squarespace element already present in the footer section. The code is modified to actually work, as opposed to other solutions found on the web.
HTML:
JAVASCRIPT: document.addEventListener('DOMContentLoaded', function() { var getURL = window.location.href; var subString = 'shop'; var badString = '/shop/p/'; let isTouch = false; const headerActions = document.querySelector('.header-actions'); function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; } checkHeader(); var searchBlock = document.getElementById('block-yui_3_17_2_1_1717927882701_20949'); if (searchBlock) { searchBlock.id = 'searchbarID'; searchBlock.classList.add('header-search-bar'); if (getURL.includes(badString) || !getURL.includes(subString)) { searchBlock.remove(); } else if (getURL.includes(subString)) { var nestedCategories = document.querySelector('.nested-category-tree-wrapper'); if (nestedCategories) { nestedCategories.insertBefore(searchBlock, nestedCategories.firstChild); } } var clonedSearchBlock = searchBlock.cloneNode(true); clonedSearchBlock.classList.add('mobile-header-search-bar'); var headerMenuNavFolderContent = document.querySelector('.header-menu-nav-folder-content'); if (headerMenuNavFolderContent) { headerMenuNavFolderContent.appendChild(clonedSearchBlock); } } }); document.addEventListener('click', function() { var getURL = window.location.href; var subString = 'shop'; var badString = '/shop/p/'; if (getURL.includes(subString) && !getURL.includes(badString)) { var allInputs = document.getElementsByTagName('input'); for (var i = 0; i < allInputs.length; i++) { allInputs[i].value = ''; } } });
CSS: .search-input { cursor: text !important; } .sqs-search-page-notice { visibility: hidden; } .sqs-search-page-notice:before { visibility: visible; content: "Congratulations! You found nothing."; text-align: center; letter-spacing: 2px; font-size: 16px; justify-content: center; @media screen and (min-width:790px) { // margin-left: 36%; } } .sqs-search-container-item:after { display: none; } @media screen and (min-width:790px) { .search-results { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); grid-column-gap: 10px; background-color: #ffc700; } .sqs-search-page-item .sqs-main-image-container { display: none; width: 50%; float: none !important; padding-right: 0 !important; } .sqs-search-page-item .sqs-main-image-container img { width: 100% !important; height: auto !important; top: 0 !important; left: 0 !important; } .sqs-search-container-item { border: 0; } .sqs-search-container-item .sqs-content { margin-top: 10px; } .sqs-search-container-item .sqs-content span { display: none; } } .sqs-search-page-input { outline-color: transparent !important; background-color: transparent !important; opacity: 100% !important; border: 2px solid #000000!important; background-image: url(https://static1.squarespace.com/static/6654b2fee26f292de13f1a9d/t/66678b0ade52f27958b0ba7a/1718061835001/Eyeicon2.png) !important; background-size: 50px !important; background-position: bottom 7px left 6px !important; background-repeat: no-repeat !important; color: #000000; text-align: left; font-size: 19px !important; letter-spacing: 2px; text-indent: 0px; cursor: pointer; border-radius: 0px; margin-bottom: 50px; @media screen and (min-width:768px) { margin-left: 35%; width: 25% !important; } } //SEARCH BARS// /* Search preview text invisible */ ::placeholder { color: #ffc700; opacity: 0; } :-ms-input-placeholder { color: #ffc700; } ::-ms-input-placeholder { color: #ffc700; } //main shop search tweaks// .search-input { transition: all .7s ease !important; background-color: transparent !important; opacity: 100% !important; border: 0px solid #000000!important; background-image: url(https://images.squarespace-cdn.com/content/v1/6654b2fee26f292de13f1a9d/2b60bbec-4d1c-4d73-9d6a-009b9bf1d26a/SearchCrosshairIcon.png) !important; background-size: 50px !important; background-position: bottom 0px left 5px !important; background-repeat: no-repeat; color: #000000; text-align: left; font-size: 19px !important; letter-spacing: 2px; text-indent: 0px; margin-left: -16px !important; margin-top: -40px; width: 240px !important; position: absolute; cursor: pointer; height: 50px; border-radius: 0px; } .search-input:focus { outline: none; transition: all .7s ease !important; background-position: bottom 0px left -6px !important; } @media only screen and (max-width:790px) { .search-input { background-position: bottom 0px left 48.5% !important; transition: all .7s ease !important; background-color: transparent !important; opacity: 100% !important; border: 0px solid #000000!important; background-size: contain; background-position: center center; background-repeat: no-repeat; color: #000000; text-align: center; font-size: 20px !important; letter-spacing: 2.5px; text-indent: -46px; margin-left: -71px !important; margin-top: 243px; width: 380px !important; height: 50px !important; position: absolute; cursor: pointer; border-radius: 0px; } .search-input:focus { background-position: bottom 0px left -7px !important; } .search-input .has-been-focused:invalid { background-position: bottom 0px left 2px !important; } } //Shop specific search// .header-search-bar{ margin-top: -100px; margin-bottom: 20px; margin-left: 0; margin-right: 0; @media(max-width:999px){ display:block; margin: 0 0 0 0vw; } } .mobile-header-search-bar { display:none; width: 80%; padding-left:10vw; padding-right:3vw; opacity:1 !important; } @media(max-width:999px){ .sqs-search-ui-button-wrapper .search-input { padding-top: 10px; padding-bottom: 10px; font-size: 1rem; width: 100%; }} /* full search page */ .sqs-search-page-input { background: #Ffc700; border: 2px solid #786F65; border-radius: 0px; } .footer-sections { overflow: hidden; // visibility: hidden !important; }
The extensive CSS is for aesthetic reasons, molding the search bar AND the search page, to fit the theme of my website. Please note that I will be replacing this solution with instant, or live search later on. This current implementation takes you to a separate page which displays search results, while my final solution will filter the products directly in their respective grid on the shop category page.
Disable right click on images only
For personal privacy reasons I wanted to initially disable the right click menu for images on my website. Although it does next to nothing for preventing users to save my images to their computers, I implemented it anyway. Notice how there is also a check for session storage, to see if the user has clicked a certain button. This will be discussed in a later post extensively. This code uses Jquery library, because I had no idea what I was doing when I wrote it, copying most of it from the web.
HTML:
JAVASCRIPT: const sacredButtonClicked = sessionStorage.getItem('sacredButtonClicked'); if (sacredButtonClicked === null || sacredButtonClicked === 'false') { $(document).ready(function () { $("body").on("contextmenu", "img", function (e) { // Prevent context menu only for images return false; }); }); }
CSS:
Interactive Ferrofluid separator
As the theme of the website is related to ink, I wanted to have animated waves of ink at the bottom of the page. I found no code which would allow me to do so with randomized wave crests, and then I stumbled upon SVG animations. The following code finds all separator elements on the page and embeds a path element which interacts with the cursor upon movement. The result is a 2D, ferrofluidic ink pool which rises as the mouse moves close to it. Currently the only separator I am using is in my footer, but I thought the idea of having it ready anywhere I want to use a separator is nice. Also when playing around with the element, it seems as if it lifts up the page, as if it is a curtain, or an incision, which I find beautiful. The code went into the footer.
HTML: <div id="wave-container"> <svg id="wave"></svg> </div>
JAVASCRIPT: document.addEventListener("DOMContentLoaded", function() { const container = document.getElementById('wave-container'); container.style.display = 'none'; //check if we are in mobile view const headerActions = document.querySelector('.header-actions'); let isTouch = false; function checkHeader() { const styles = window.getComputedStyle(headerActions); isTouch = styles.getPropertyValue('display') !== 'flex'; } checkHeader(); if (isTouch !== true) { const lines = document.querySelectorAll('.horizontalrule-block'); lines.forEach(line => { const clonedContainer = container.cloneNode(true); line.appendChild(clonedContainer); clonedContainer.style.display = 'block'; const svg = clonedContainer.querySelector('#wave'); const width = clonedContainer.clientWidth; const height = clonedContainer.clientHeight / 2; const waveHeight = 0; const numPoints = 300; const points = []; for (let i = 0; i <= numPoints; i++) { points.push({ x: (i / numPoints) * width, y: height, originalY: height + Math.random() * waveHeight - waveHeight / 2 }); } const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); svg.appendChild(path); function createPath(points) { let d = `M ${points[0].x} ${points[0].y}`; for (let i = 1; i < points.length; i++) { d += ` L ${points[i].x} ${points[i].y}`; } return d; } function animate() { for (let i = 0; i < points.length; i++) { points[i].y += (points[i].originalY - points[i].y) * 0.05; } path.setAttribute('d', createPath(points)); requestAnimationFrame(animate); } clonedContainer.addEventListener('mousemove', function(event) { const rect = clonedContainer.getBoundingClientRect(); const mouseX = event.clientX - rect.left; const mouseY = event.clientY - rect.top; for (let i = 0; i < points.length; i++) { const dx = mouseX - points[i].x; const dy = mouseY - points[i].y; const distance = Math.sqrt(dx * dx + dy * dy); const force = Math.exp(-distance * 0.045); const scale = Math.min(i / numPoints, 1 - (i / numPoints)); points[i].y -= force * 20 * scale; // Apply scaled force } }); animate(); }); } });
CSS: .horizontalrule-block { justify-content: center; position: relative; z-index: 9999999; } #wave-container { width: 75%; height: 200px; display: flex; justify-content: center; align-items: center; position: absolute; left: 50%; transform: translateX(-50%); padding: 0; } #wave-container svg { width: 100%; height: 100%; top: 0; position: absolute; } #wave-container path { fill: #000000; stroke: transparent; stroke-width: 2; }
Notice how there is a variable for the number of points. This allows potential scaling for devices which are less powerful. However, I currently disable this code on mobile as it hits differently on touch screens.
COMMENTS:
Leave a comment: