/**
* Creates sticker elements along-side pre-defined DOM element(s)
*
*
* @extends HTMLElement
*
* @author Peter G. Darmis <pdarmis@gmail.com>
* @license MIT
*/
export class pdSticker extends HTMLElement {
/**
* Define what are the elements observed attributes, custom or non-custom.
* .
* @return {void}
*/
static get observedAttributes() {
return ['direction', 'search-for'];
}
/**
* The element's constructor. Some basic properties are initialized here.
* .
* @return {void}
*/
constructor() {
super();
this.sRoot = this.attachShadow({
mode: 'closed'
});
let direction = this.getAttribute("direction");
let searchFor = this.getAttribute("search-for");
this.direction = !this.isEmpty(direction) ? direction : 'left';
this.searchFor = !this.isEmpty(searchFor) ? searchFor : 'article';
this.stickers = [];
}
/**
* Add a button based on pd-indicator custom web component.
* This means that pd-indicator must be imported and that a data-button attribute has been placed in your HTML.
* e.g. data-button="pd-indicator-switch,pd-indicator-heart,pd-indicator-circle,pd-indicator-octastar,pd-indicator-yin-yang"
*
* @param {string} button - A comma separated list of pd-indicator styles.
* @param {HTMLDivElement} div - An HTML <div> element that the pd-indicator button will be placed.
* @return {void}
*/
addIndicator(button, div) {
let regex = /pd-indicator-/g;
if (regex.test(button)) {
let type = button.replace("pd-indicator-", "");
if (['moon', 'yin-yang', 'pointer', 'rectangle', 'circle', 'pacman', 'octastar', 'infinity', 'heart', 'switch', 'donut'].includes(type)) {
if (window.customElements.get('pd-indicator')) {
let indicator = new pdIndicator();
indicator.setAttribute('type', type);
div.appendChild(indicator);
}
}
}
}
/**
* Changes the direction of the stickers' side. It is either "left" or "right".
*
* @return {void}
*/
changeDirection() {
let self = this;
let searchFor = document.querySelector(self.searchFor);
let initialOffsetTop = 2*Math.max(searchFor.parentElement.offsetTop,searchFor.offsetParent.offsetTop);
let initialOffsetLeft = Math.max(searchFor.parentElement.offsetLeft,searchFor.offsetParent.offsetLeft);
if(self.isEmpty(initialOffsetTop)) {
initialOffsetTop = 0;
}
if(self.isEmpty(initialOffsetLeft)) {
initialOffsetLeft = 0;
}
window.scrollTo(initialOffsetTop,400);
let items = document.querySelectorAll(self.searchFor);
let scroll_distance = [];
self.sRoot.querySelectorAll("div.sticker").forEach((sticker, index) => {
self.applyPosition(sticker, items[index], index, scroll_distance, initialOffsetLeft, initialOffsetTop);
});
}
/**
* Initialize the module. After the module instance has been created, here is were most work is done.
* Styling will be applied.
* All stickers will be created and displayed at the proper position.
* At the same time the scroll distance of each sticker will have been calculated.
*
* @return {void}
*/
init() {
let self = this;
let searchFor = document.querySelector(self.searchFor);
let initialOffsetTop = 2*Math.max(searchFor.parentElement.offsetTop,searchFor.offsetParent.offsetTop);
let initialOffsetLeft = Math.max(searchFor.parentElement.offsetLeft,searchFor.offsetParent.offsetLeft);
if(self.isEmpty(initialOffsetTop)) {
initialOffsetTop = 0;
}
if(self.isEmpty(initialOffsetLeft)) {
initialOffsetLeft = 0;
}
window.scrollTo(initialOffsetTop,400);
let scroll_distance = [];
if (!self.isEmpty(self.sRoot.querySelector("style"))) {
self.sRoot.querySelector("style").remove();
}
if (!self.isEmpty(self.sRoot.querySelector("div.sticker"))) {
self.sRoot.querySelectorAll("div.sticker").forEach(item => item.remove());
}
if (self.isEmpty(self.sRoot.querySelector("style"))) {
let style = document.createElement("style");
style.innerHTML = "pd-indicator, a, button { cursor: pointer; } .sticker img, .sticker video { width: 3rem; height: auto; } .sticker { font-size: 2vmin; } .note { text-decoration: none; background: #ffc; box-shadow: .375rem .375rem .5rem rgba(33,33,33,.7); } .note.rotatedLeft { transform: rotate(-6deg); } .note.rotatedRight { transform: rotate(6deg); } .speech-bubble-left, .speech-bubble-right { backdrop-filter: blur(0.25rem); background: rgba(4, 17, 32, 1); box-shadow: .375rem .375rem .5rem rgba(33,33,33,.7); border-radius: .5rem } .speech-bubble-left:before { content: \"\"; position: absolute; right: 100%; top: 1.5rem; width: 0; height: 0; border-top: .5rem solid transparent; border-right: 1rem solid rgba(4, 17, 32, 1); border-bottom: .5rem solid transparent; } .speech-bubble-right:after { content: \"\"; position: absolute; left: 100%; top: 1.5rem; width: 0; height: 0; border-top: .5rem solid transparent; border-left: 1rem solid rgba(4, 17, 32, 1); border-bottom: .5rem solid transparent; }.speech-bubble-left, .speech-bubble-left *, .speech-bubble-right, .speech-bubble-right * { color: #ffffff; } .black-box { color: #ffffff; padding: .125rem; list-style: none; box-shadow: .375rem .375rem .5rem rgba(33,33,33,.7); background: rgba(4, 18, 27, 0.88); border-radius: .25rem; } .speech-bubble-left, .speech-bubble-right, .black-box { padding: 0.5rem; }";
self.sRoot.appendChild(style);
}
document.querySelectorAll(self.searchFor).forEach((item, index) => {
let id = !self.isEmpty(item.id) ? item.id : index ;
let div = document.createElement("div");
div.id = "sticker-" + id;
div.classList.add("sticker");
div.style.marginTop = 0;
div.style.maxWidth = '8rem';
div.style.width = 'auto';
div.style.minHeight = '5rem';
div.style.height = 'auto';
div.style.textAlign = 'center';
div.style.flexDirection = 'column';
div.style.alignItems = 'center';
div.style.alignContent = 'center';
div.style.justifyContent = 'center';
div.style.display = 'none';
if (item.title) {
let regex = /(<([^>]+)>)/ig;
item.title = item.title.replace(regex, "");
if (item.dataset.link) {
div.innerHTML = '<a href="' + item.dataset.link + '">' + item.title + '</a>';
} else {
div.innerHTML = item.title;
}
}
if (item.dataset.img) {
if (item.dataset.link) {
div.innerHTML = '<a href="' + item.dataset.link + '"><img src="' + item.dataset.img + '"/></a>';
} else {
div.innerHTML = '<img src="' + item.dataset.img + '"/>';
}
}
if (item.dataset.button) {
let buttons = item.dataset.button;
if (item.dataset.button.indexOf(",") !== -1) {
buttons = item.dataset.button.split(",").filter((word) => {
return word.length > 0;
});
}
if (Array.isArray(buttons)) {
buttons.forEach((button) => {
self.addIndicator(button, div);
})
} else {
self.addIndicator(buttons, div);
}
}
div.style.position = "absolute";
div.style.top = (initialOffsetTop/ 16) + 'rem';
let slot = document.createElement("slot");
slot.name = "sticker-"+index;
div.append(slot);
self.sRoot.append(div);
let sticker = self.sRoot.querySelector("#sticker-" + id);
sticker.style.display = 'flex';
self.applyPosition(sticker, item, index, scroll_distance, initialOffsetLeft, initialOffsetTop);
});
}
/**
* This is a function that acts as a helper function to changeDirection() and init().
* Styling will be applied.
* All stickers will be created and displayed at the proper position.
* At the same time the scroll distance of each sticker will have been calculated.
*
* @param {HTMLDivElement} sticker - An <pd-sticker> HTML <div> sub-element that is the sticker object displayed at the front-end.
* @param {HTMLElement} item - An HTML element that the associated sticker object created by pd-sticker will be placed.
* @param {number} index - The index of the item HTML element above.
* @param {array} scroll_distance - An empty array that will be filled with the values of available scroll distance for each sticker object.
* @param {number} initialOffsetLeft - The initial left offset of each sticker object.
* @param {number} initialOffsetTop - The initial top offset of each sticker object.
* @return {void}
*/
applyPosition(sticker, item, index, scroll_distance, initialOffsetLeft, initialOffsetTop) {
let self = this;
scroll_distance[index] = item.offsetHeight - sticker.offsetHeight;
if (self.direction == 'right') {
sticker.style.left = (100*(window.innerWidth - (item.offsetLeft + initialOffsetLeft)) / window.innerWidth) + '%';
} else {
sticker.style.left = (100*(initialOffsetLeft + parseFloat(window.getComputedStyle(item, null).getPropertyValue("padding-left")) - parseFloat(window.getComputedStyle(sticker, null).getPropertyValue("width")))/window.innerWidth) + '%';
}
if (!self.isEmpty(item.hasChildNodes())) {
let children = item.childNodes;
if (children.length > 1) {
for (let i = 0; i < children.length; i++) {
if (!self.isEmpty(children[i].tagName)) {
children[i].addEventListener("load", (e) => {
scroll_distance[index] += !self.isEmpty(children[i].clientHeight) ? children[i].clientHeight - sticker.clientHeight : 0;
self.duringScroll(sticker, item, scroll_distance[index], initialOffsetTop);
});
}
}
self.duringScroll(sticker, item, scroll_distance[index], initialOffsetTop);
} else {
self.duringScroll(sticker, item, scroll_distance[index], initialOffsetTop);
}
} else {
self.duringScroll(sticker, item, scroll_distance[index], initialOffsetTop);
}
}
/**
* This is a function that acts as a helper function to applyPosition().
* It helps on smoother animation of the stickers' movement while scrolling.
* It also manages the behavior of each sticker. If it has an absolute position or fixed while scrolling.
*
* @param {HTMLDivElement} sticker - An <pd-sticker> HTML <div> sub-element that is the sticker object displayed at the front-end.
* @param {HTMLElement} item - An HTML element that the associated sticker object created by pd-sticker will be placed.
* @param {array} scroll_distance - An array containing the values of available scroll distance for each sticker object.
* @param {number} initialOffsetTop - The initial top offset of each sticker object.
* @return {void}
*/
duringScroll(sticker, item, scroll_distance, initialOffsetTop = 0) {
let scroll = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) {
window.setTimeout(callback, 1000 / 72)
};
window.addEventListener("scroll", () => {
scroll(() => {
if (document.documentElement.scrollTop >= (item.offsetTop + initialOffsetTop) && document.documentElement.scrollTop <= (item.offsetTop + scroll_distance + initialOffsetTop)) {
sticker.style.position = 'fixed';
sticker.style.top = '0.5rem';
} else {
sticker.style.position = 'absolute';
if (document.documentElement.scrollTop > (item.offsetTop + initialOffsetTop)) {
sticker.style.top = ((item.offsetTop + initialOffsetTop + scroll_distance) / 16) + 'rem';
} else {
sticker.style.top = ((item.offsetTop + initialOffsetTop) / 16) + 'rem';
}
}
});
});
}
/**
* This is a function that acts as a helper function in general.
* It checks if a value is "empty" in case of many types of input.
*
* @param {?(number|boolean|array|object|string)} value - An multiple type value to be checked.
* @return {boolean}
*/
isEmpty(value) {
switch (true) {
case (value == null || value == undefined):
return true;
case (Array.isArray(value)):
return value.length == 0;
case (typeof value == 'object'):
return (Object.keys(value).length === 0 && value.constructor === Object);
case (typeof value == 'string'):
return value.length == 0;
case (typeof value == 'number' && !isNaN(value)):
return value == 0;
case (!value):
return true;
default:
return false;
}
}
/**
* Invoked each time the custom element is appended into a document-connected element.
* This will happen each time the node is moved, and may happen before the element's contents have been fully parsed.
*
* @return {void}
*/
connectedCallback() {
let direction = this.getAttribute("direction");
let searchFor = this.getAttribute("search-for");
this.direction = !this.isEmpty(direction) ? direction : 'left';
this.searchFor = !this.isEmpty(searchFor) ? searchFor : 'article';
this.init();
window.onresize = () => {
this.changeDirection();
}
window.onorientationchange = () => {
this.changeDirection();
}
}
/**
* Invoked each time the custom element is disconnected from the document's DOM.
*
* @return {void}
*/
disconnectedCallback() {
console.log('Disconnected.');
}
/**
* Invoked each time the custom element is moved to a new document.
*
* @return {void}
*/
adoptedCallback() {
console.log('Adopted.');
}
/**
* Invoked each time one of the custom element's attributes is added, removed, or changed.
* Which attributes to notice change for is specified in a static getobservedAttributes() method
*
* @param {string} name - The element's attribute name.
* @param {string} oldValue - The old attribute value.
* @param {string} newValue - The new attribute value.
* @return {void}
*/
attributeChangedCallback(name, oldValue, newValue) {
if (name == "direction") {
let direction = this.getAttribute("direction");
this.direction = !this.isEmpty(direction) ? direction : 'left';
this.changeDirection();
}
if (name == "search-for") {
let searchFor = this.getAttribute("search-for");
this.searchFor = !this.isEmpty(searchFor) ? searchFor : 'article';
}
}
}
if (!window.customElements.get('pd-sticker')) {
window.pdSticker = pdSticker;
window.customElements.define("pd-sticker", pdSticker);
}