granulate
Version:
Did you ever have to divide your pile of content into horizontal containers, slides, panels?
478 lines (376 loc) • 20.4 kB
JavaScript
export default class Granulate {
//*******************************************************
// building the Granulate Object
//*******************************************************
constructor(options) {
// merge settings
this.settings = Granulate.mergeSettings(options);
// store some data in the class object
this.input = {};
this.storeData(this.settings.content);
// check the configuration
if(!this.input.container){
console.warn('granulate.js: please define your content');
}
if(!this.output.container){
console.warn('granulate.js: please define your outputContainer');
}
// inititate the class if all features supported
//------------------------------------------------------
if (Granulate.featureTest()) {
this.settings.beforeGranulated();
this.attachEvents();
this.injectStyles();
this.wrapIntoSpans(function(classObject){
classObject.buildStorePasteLines(classObject.input.container, classObject, function(classObject){
classObject.granulateContent(classObject, function(){
//lets call the callback with some new data
classObject.settings.afterGranulated.call(classObject)
});
});
});
} else {
console.warn('your browser doesnt support Granulate')
}
}
//*******************************************************
// Default settings
//*******************************************************
static mergeSettings(options) {
const settings = {
content: '.granulate',
outputContainerHeight: 400,
outputContainerCss: 'granulate-output',
listClass: 'granulate-list-item',
dontSplit:'granulate-hold',
outputInlineCss: '',
beforeGranulated: function(){},
afterGranulated: function(){},
beforeResize: function(){},
afterResize: function(){}
};
const userSttings = options;
for (const attrname in userSttings) {
settings[attrname] = userSttings[attrname];
}
return settings;
}
//*******************************************************
// helper methods
//*******************************************************
// Attach events
//------------------------------------------------------
attachEvents() {
// Resize element on window resize
window.addEventListener('resize', this.resizeHandler.bind(this));
}
// update
//------------------------------------------------------
resizeHandler(){
this.settings.beforeResize();
const container = this.output.container;
container.innerHTML = this.input.spanedContent.outerHTML;
container.setAttribute('style', '');
this.storeData(container.childNodes[0]);
// console.log(this.input.container);
this.buildStorePasteLines(this.input.container, this, function(classObject){
classObject.granulateContent(classObject, function(){
//lets call the callback with some new data
classObject.settings.afterResize.call(classObject)
// console.log(classObject);
});
});
}
// store data in the class object
//------------------------------------------------------
storeData(inputContentObject){
this.input.padding={};
this.input.container = typeof inputContentObject === 'string' ? document.querySelector(inputContentObject) : inputContentObject;
this.input.width = Granulate.getStylesValues(this.input.container, 'width');
this.input.padding.top = Granulate.getStylesValues(this.input.container, 'padding-top');
this.input.padding.bottom = Granulate.getStylesValues(this.input.container, 'padding-bottom');
this.input.padding.left = Granulate.getStylesValues(this.input.container, 'padding-left');
this.input.padding.right = Granulate.getStylesValues(this.input.container, 'padding-right');
this.output = {};
this.output.container = this.input.container.parentNode;
}
// feature test
//------------------------------------------------------
static featureTest(){
return 'querySelector' in document && 'addEventListener' in window;
}
// replaceHtml
//------------------------------------------------------
static replaceHtml(object, html, callback){
object.innerHTML = html;
if (typeof callback === "function") {
callback();
}
}
// get Computed styles
//------------------------------------------------------
static getStyles(obj, attribute){
const object = (typeof window.getComputedStyle === 'undefined') ? obj.currentStyle : window.getComputedStyle(obj);
const value = object.getPropertyValue(attribute)
return value;
}
static getStylesValues(obj, attribute){
const object = (typeof window.getComputedStyle === 'undefined') ? obj.currentStyle : window.getComputedStyle(obj);
const value = object.getPropertyValue(attribute)
const valueNumber = Number(value.slice(0, -2));
return valueNumber;
}
// after document finished repainting
// let's pass in the granulate object to
// be able to use callbacks
//------------------------------------------------------
afterDocumentComplete(classObject, callback){
let readyStateCheckInterval = setInterval(function(classObject) {
if (document.readyState === "complete") {
clearInterval(readyStateCheckInterval);
callback();
}
}, 10);
}
//*******************************************************
// Main Methods
//*******************************************************
// styles for proper measuring
//------------------------------------------------------
injectStyles(){
const css = `
${this.settings.outputContainer} span { display:inline-block;}
`;
const head = document.head || document.getElementsByTagName('head')[0];
const styles = document.createElement('style');
styles.type = 'text/css';
if (styles.styleSheet){
styles.styleSheet.cssText = css;
} else {
styles.appendChild(document.createTextNode(css));
}
head.appendChild(styles);
}
// This function will wrap all the words from the content container into spans except for
// already existing spans, lists, or predefined tags (settings.dontSplit)
//--------------------------------------------------------------
wrapIntoSpans(callback){
const gContent = this.input.container;
const gContentWidth = Granulate.getStyles(gContent, 'width')
const gContentPadding = Granulate.getStyles(gContent, 'padding')
const gContentChildren = gContent.children;
// iterate through all children
for (let i = 0; i < gContentChildren.length; i++) {
const child = gContentChildren[i];
const childTagname = child.tagName.toLowerCase();
const childHtml = child.innerHTML;
const childClasses = child.classList;
// we dont wrap words in <li>'s and predefined nodes
const dontIgnore = childTagname !== 'ul' && childTagname !=='ol' && !childClasses.contains(this.settings.dontSplit);
if(dontIgnore){
const nodes = child.childNodes;
let array = [];
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].nodeName == "#text") {
array = array.concat(nodes[i].nodeValue.split(" "));
continue;
}
array.push(nodes[i].outerHTML);
}
const nodesArray = array.filter(v => v.length > 0);
const newText = nodesArray.map(function(node){
const notString = node.indexOf('<span') > -1;
//don't wrap spans with spans
if (notString) {
return `${node}`;
} else {
return `<span class="node">${node}</span>`; // make sure the span wont exceed the parent (display:inline-block)
}
}).join(' ');
// let's replace the children with the new "spaned" content
child.innerHTML = newText;
}
}
const classObject = this;
// lets cache the spaned content as an object for updates (resize etc..)
const spanedInput = document.createElement('div');
[].slice.call(gContent.attributes).forEach(function(item) {
spanedInput.setAttribute(item.name, item.value)
});
spanedInput.innerHTML = gContent.innerHTML
// cache it in the Granulate Object
this.input.spanedContent = spanedInput
console.log(this);
// callback after the page finished repainting
this.afterDocumentComplete(classObject, function(){
if (typeof callback === "function") {
callback(classObject);
}
})
}
// let's build an array of lines objects based on the spans offset.
// The line items will be stored as a string built from an array or a string
// Granulate.lines = [
// {
// tagName: '';
// cssClasses:'';
// styling: '';
// items: [].join(' '); //join if items is an array
// },
// ]
//------------------------------------------------------
buildStorePasteLines(container, classObject, callback){
// VARS
const nodesChildren = container.children;
const settings = this.settings;
classObject.lines = [];
let lines = [];
let line = {};
let lastTop = 0;
let lineItems = [];
//Methods
function createLineAndStoreInLines(tag, css, styles, items){
// ignored line.items wont be stored as arrays but as strings
const itemsString = typeof items === 'object' ? items.join(' ') : items
if (items.length > 0) {
line = {
tagName: tag,
cssClasses: css,
styling: styles,
items: itemsString
}
lines.push(line);
// console.log('createLine from:', tag, '-', items);
}
}
//iterate through all nodes
for (let i = 0; i < nodesChildren.length; i++) {
const child = nodesChildren[i];
const childTagname = child.tagName.toLowerCase();
const childClassList = child.classList;
const childInlineStyles = child.getAttribute('styles');
const childHtml = child.innerHTML;
const childMarginBottom = window.getComputedStyle(child, null).getPropertyValue('margin-bottom');
// we wont split li's
if( childTagname == 'ul' || childTagname =='ol' ){
const newChildHtml = childHtml.split(/(?=<li>)/);
newChildHtml.map(function(li){
const notEmpty = li.indexOf('</li>') > -1 && li.indexOf('<li>') > -1;
if (notEmpty) {
lineItems = [];
lineItems.push(li.replace('<li>', '').replace('</li>', '').replace('\n', '').replace(/ /, '').trim());
createLineAndStoreInLines('p', settings.listClass, '', lineItems)
}
});
lineItems = [];
// console.warn(`granulate.js: Please style your new list markup: .${settings.listClass}`);
} else if( (childClassList.value).indexOf(settings.dontSplit) > -1 ){
createLineAndStoreInLines(childTagname, childClassList.value, childInlineStyles, childHtml )
} else { // we will split all other tags
const words = child.querySelectorAll('span');
const lastWord = words.length -1;
lineItems = [];
for (let i = 0; i < words.length; i++) {
const word = words[i];
const toBeStriped = word.classList.contains('node');
const html = word.innerHTML;
const outerHtml = word.outerHTML;
const isLastWord = (i == lastWord);
const isFirstWord = (i == 0);
if (isFirstWord) {
lastTop = word.offsetTop;
}
const offset = word.offsetTop;
const min = lastTop - 2;
const max = lastTop + 2;
const isOnOneLine = (min <= offset) && (offset <= max);
// console.log(`${offset} : ${lastTop}, ${isOnOneLine}, ${childTagname}, ${html} || ${lineItems}`);
if (isLastWord && isOnOneLine) {
if (toBeStriped) {
lineItems.push(html)
} else {
lineItems.push(outerHtml);
}
createLineAndStoreInLines(childTagname, childClassList.value, 'margin-bottom:0;', lineItems);
lineItems = []
lastTop = word.offsetTop;
// console.log('LAST');
}
if (isFirstWord && !isOnOneLine) {
if (toBeStriped) {
lineItems.push(html)
} else {
lineItems.push(outerHtml);
}
}
// Next line
if (!isOnOneLine) {
createLineAndStoreInLines(childTagname, childClassList.value, 'margin-bottom:0;', lineItems);
lineItems = []
lastTop = word.offsetTop;
}
if (toBeStriped) {
lineItems.push(html)
} else {
lineItems.push(outerHtml);
}
if (isLastWord) {
// let's add the original margin to the last line
const currentLine = lines.length - 1;
lines[currentLine].styling = `margin-bottom: ${childMarginBottom}`;
lineItems = []
}
}
}
}
// let's store the lines in the Object
classObject.lines = lines;
//lets build the markup
classObject.linesMarkup = classObject.lines.map(function(line){
return `<${line.tagName} style="${line.styling}" class="${line.cssClasses}">${line.items}</${line.tagName}>`;
}).join('');
// let's paste the markup
classObject.input.container.innerHTML = classObject.linesMarkup;
this.afterDocumentComplete(classObject, function(){
if (typeof callback === "function") {
callback(classObject);
}
})
}
// Lets divide the content into containers with a defined height
//------------------------------------------------------
granulateContent(classObject, callback){
const padding = this.input.padding;
// console.log(padding);
const containerSpace = this.settings.outputContainerHeight - padding.top - padding.bottom;
const children = this.input.container.children;
let globalHtml = `<div style="${this.settings.outputInlineCss}; width:${this.input.width}px; padding-top:${padding.top}px; padding-bottom:${padding.bottom}px; padding-left:${padding.left}px; padding-right:${padding.right}px; height:${this.settings.outputContainerHeight}px" class="${this.settings.outputContainerCss}">`;
let currentHeight = 0;
let breakFlag = false;
let numberOfPanels = 1;
for (let i = 0; i < children.length; i++) {
const _this = children[i];
const marginBottom = Granulate.getStylesValues(_this, 'margin-bottom');
const marginTop = Granulate.getStylesValues(_this, 'margin-top');
const height = _this.clientHeight + marginTop + marginBottom;
const html = _this.outerHTML;
console.log(`${currentHeight} + ${height} = ${currentHeight + height} - ${_this.innerText}`);
currentHeight = height + currentHeight;
if (currentHeight < containerSpace) {
globalHtml += html;
} else {
//reset the height counter
currentHeight = height;
numberOfPanels ++;
globalHtml += `</div><div style="${this.settings.outputInlineCss}; width:${this.input.width}px; padding-top:${padding.top}px; padding-bottom:${padding.bottom}px; padding-left:${padding.left}px; padding-right:${padding.right}px; height:${this.settings.outputContainerHeight}px" class="${this.settings.outputContainerCss}"> ${html}`;
}
}
globalHtml += `</div>`;
this.granulated = globalHtml;
this.output.container.innerHTML = this.granulated;
this.output.numberOfPanels = numberOfPanels
if (typeof callback === "function") {
callback(classObject);
}
}
}