/* jshint latedef:nofunc */
/* jshint unused:false*/
/**
*
* @param config
* @constructor
*/
(function (global, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
// For CommonJS and CommonJS-like environments where a proper `window`
// is present, execute the factory and get Rulez.
// For environments that do not have a `window` with a `document`
// (such as Node.js), expose a factory as module.exports.
// This accentuates the need for the creation of a real `window`.
// e.g. var Rulez = require('rulez.js')(window);
module.exports = global.document ?
factory(global, true) :
function (w) {
if (!w.document) {
throw new Error('rulez.js requires a window with a document');
}
return factory(w);
};
} else {
factory(global);
}
// Pass this if window is not defined yet
}(typeof window !== 'undefined' ? window : this, function (window, noGlobal) {
var Rulez = function (config) {
'use strict';
var svgNS = 'http://www.w3.org/2000/svg';
var defaultConfig = {
width: null,
height: null,
element: null,
layout: 'horizontal',
alignment: 'top',
units: '', //'em', 'ex', 'px', 'pt', 'pc', 'cm', 'mm', 'in' and ''(user units) : http://www.w3.org/TR/SVG/coords.html#Units
divisionDefaults: {
strokeWidth: 1,
type: 'rect',
className: 'rulez-rect',
renderer: null
},
textDefaults: {
rotation: 0,
offset: 25,
className: 'rulez-text',
/**
* Wherever to show or not to show units alongside text
*/
showUnits: false,
centerText: true,
renderer: null
},
guideDefaults: {
strokeWidth: 1,
getSize: function() {
return 5000;
}
},
divisions: [
{
pixelGap: 5,
lineLength: 5
},
{
pixelGap: 25,
lineLength: 10
},
{
pixelGap: 50,
lineLength: 15
},
{
pixelGap: 100,
lineLength: 20
}
],
texts: [
{
pixelGap: 100
}
],
guides: [],
guideSnapInterval: 10
};
var getDefaultConfigCopy = function () {
var copy = JSON.parse(JSON.stringify(defaultConfig));
copy.guideDefaults.getSize = defaultConfig.guideDefaults.getSize;
return copy;
};
/**
* result config
*/
var c = mergeConfigs(getDefaultConfigCopy(), config);
if (!c.guideDefaults.className) {
if (isVertical()) {
c.guideDefaults.className = 'rulez-guide-vert';
} else {
c.guideDefaults.className = 'rulez-guide-horiz';
}
}
c = mergeConfigs(c, c);
/**
* amount of additional(redundant) divisions on left and right (top, bottom) side of ruler
*/
var additionalDivisionsAmount = 2;
/**
* main group (g svg element) that contains all divisions and texts
* @type {SVGGElement}
*/
var g = createGroup();
/**
* Array of arrays of all texts
* @type {Array.<Array.<SVGTextElement >>}
*/
var texts = [];
/**
* Array of all guides
* @type {Array.<SVGTextElement>}
*/
var guides = [];
/**
* Current position of ruler
* @type {number}
*/
var currentPosition = 0;
/**
* Start position of drawing ruler
* @type {number}
*/
var startPosition;
/**
* End position of drawing ruler
* @type {number}
*/
var endPosition;
/**
* Scale of ruler
* @type {number}
*/
var scale = 1;
var size;
var maxDistance = 0;
var unitConversionRate;
/**
* Renders ruler inside svg element
*/
this.render = function () {
c.width || (c.width = c.element.getBoundingClientRect().width);
c.height || (c.height = c.element.getBoundingClientRect().height);
c.element.appendChild(g = createGroup());
size = isVertical() ? c.height : c.width;
unitConversionRate = getUnitConversionRate();
calculateStartEndPosition();
generateDivisionsAndTexts(startPosition, endPosition);
generateGuides();
this.scrollTo(0, false);
c.element.addEventListener('dblclick', function (e) {
var position = isVertical() ? e.offsetY : e.offsetX;
position = (currentPosition + position) * scale;
var guideConfig = Object.assign({
position: position
}, c.guideDefaults);
c.guides.push(guideConfig);
createGuideFromConfig(guideConfig);
});
};
/**
* Scrolls ruler to specified position.
* @param {number} pos left(or top for vertical rulers) position to scroll to.
* @param {boolean} useUnits if true pos will be multiplied by unit conversion rate;
*/
this.scrollTo = function (pos, useUnits) {
currentPosition = pos;
if (useUnits) {
currentPosition *= unitConversionRate;
}
if (isVertical()) {
g.setAttribute('transform', 'translate(0,' + (-currentPosition % (maxDistance * unitConversionRate)) + ')');
} else {
g.setAttribute('transform', 'translate(' + (-currentPosition % (maxDistance * unitConversionRate)) + ',0)');
}
var pixelCurrentPosition = currentPosition / unitConversionRate;
for (var i = 0; i < c.texts.length; i++) {
var textConfig = c.texts[i];
var textElements = texts[i];
var amountPerMaxDistance = maxDistance / textConfig.pixelGap;
var offset = pixelCurrentPosition % maxDistance;
var startTextPos = pixelCurrentPosition - offset;
for (var j = 0; j < textElements.length; j++) {
var textElement = textElements[j];
var text = Math.floor((startTextPos + (j - additionalDivisionsAmount * amountPerMaxDistance) * textConfig.pixelGap) * scale);
if (textConfig.showUnits) {
text = addUnits(text);
}
textElement.textContent = text;
if (textConfig.renderer) {
textConfig.renderer(textElement);
}
}
}
for (i = 0; i < guides.length; i++) {
moveGuide(guides[i], c.guides[i]);
}
};
/**
* Scales the ruler's text values by specific value.
* @param {number} scaleValue
*/
this.setScale = function (scaleValue) {
scale = scaleValue;
this.scrollTo(currentPosition, false);
};
/**
* Updates size with current clientWidth(height) in case it's bigger than previous one.
* Only appends more divisions and texts if necessary.
*/
this.resize = function () {
var oldSize = size;
var newSize = isVertical() ? c.element.clientHeight : c.element.clientWidth;
if (oldSize !== newSize) {
if (oldSize > newSize) {
//todo remove redundant divisions?
} else {
size = newSize;
var oldEndPosition = endPosition;
calculateStartEndPosition();
generateDivisionsAndTexts(oldEndPosition, endPosition);
this.scrollTo(currentPosition, false);
}
}
//FIXME guide resize
};
this.getGuideConfigs = function () {
return JSON.parse(JSON.stringify(c.guides));
};
/**
* Callback that is called after saving of ruler as image is done
* @callback saveFinishCallback
* @param {string} base64 png image string
*/
/**
* Saves ruler as image.
* @param {saveFinishCallback} saveFinishCallback
*/
this.saveAsImage = function (saveFinishCallback) {
var svgClone = deepCloneWithCopyingStyle(c.element);
//http://stackoverflow.com/questions/23514921/problems-calling-drawimage-with-svg-on-a-canvas-context-object-in-firefox
svgClone.setAttribute('width', c.width);
svgClone.setAttribute('height', c.height);
//
var canvas = window.document.createElement('canvas');
canvas.setAttribute('width', c.width);
canvas.setAttribute('height', c.height);
var ctx = canvas.getContext('2d');
var URL = window.URL || window.webkitURL;
var img = new Image();
img.style.position = 'absolute';
img.style.top = '-100000px';
img.style.left = '-100000px';
img.style.zIndex = -100000;
img.setAttribute('width', c.width);
img.setAttribute('height', c.height);
var svg = new Blob([svgClone.outerHTML], {type: 'image/svg+xml;charset=utf-8'});
var url = URL.createObjectURL(svg);
img.onload = function () {
setTimeout(function () { //workaround for not working width and height.
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
window.document.body.removeChild(img);
saveFinishCallback(canvas.toDataURL());
}, 1000);
};
window.document.body.appendChild(img);
img.src = url;
};
/**
* @returns {number} how much pixels are in used unit.
*/
this.getUnitConversionRate = function () {
return getUnitConversionRate();
};
function deepCloneWithCopyingStyle(node) {
var clone = node.cloneNode(false);
var i;
if (node instanceof Element) {
var computedStyle = window.getComputedStyle(node);
if (computedStyle) {
for (i = 0; i < computedStyle.length; i++) {
var property = computedStyle[i];
clone.style.setProperty(property, computedStyle.getPropertyValue(property), '');
}
}
}
for (i = 0; i < node.childNodes.length; i++) {
clone.appendChild(deepCloneWithCopyingStyle(node.childNodes[i]));
}
return clone;
}
function calculateStartEndPosition() {
if (!maxDistance) {
c.divisions.forEach(function (entry) {
if (entry.pixelGap > maxDistance) {
maxDistance = entry.pixelGap;
}
});
}
endPosition = size - (size % maxDistance) + maxDistance * additionalDivisionsAmount;
startPosition = -maxDistance * additionalDivisionsAmount;
}
function generateDivisionsAndTexts(startPosition, endPosition) {
c.divisions.forEach(function (division) {
generateDivisions(startPosition, endPosition, division);
});
var i = 0;
c.texts.forEach(function (textConfig) {
var textsArray = generateTexts(startPosition, endPosition, textConfig);
if (texts[i]) {
texts[i] = texts[i].concat(textsArray);
} else {
texts.push(textsArray);
}
i++;
});
}
function generateDivisions(startPosition, endPosition, elementConfig) {
for (var i = startPosition; i < endPosition; i += elementConfig.pixelGap) {
var line = createLine(i, elementConfig);
g.appendChild(line);
if (elementConfig.renderer) {
elementConfig.renderer(line);
}
}
}
function generateGuides() {
c.guides.forEach(function (guideConfig) {
createGuideFromConfig(guideConfig);
});
}
function createGuideFromConfig(guideConfig) {
var guide = generateGuide(guideConfig);
guides.push(guide);
makeMovable(guide, guideConfig);
g.appendChild(guide);
if (guideConfig.renderer) {
guideConfig.renderer(guide);
}
}
function moveGuide(guide, guideConfig) {
var offset = (-currentPosition) % (maxDistance * unitConversionRate);
var position = guideConfig.position / scale - currentPosition - offset ;
guide.setAttribute('transform', isVertical() ? 'translate(0,' + position + ')' : 'translate(' + position + ',0)');
}
/**
*
* @param {SVGElement} guide
*/
function makeMovable(guide, guideConfig) {
var startPos;
var startGuidePos = guideConfig.position;
var isVerticalRuler = isVertical();
var posPropName = isVerticalRuler ? 'pageY' : 'pageX';
var globalClassName = isVerticalRuler ? 'rulez-guide-vert-global' : 'rulez-guide-horiz-global';
var positionPrefix = isVerticalRuler ? 'Y : ' : 'X : ';
var leftPositionMargin = isVerticalRuler ? 10 : 0;
var topPositionMargin = isVerticalRuler ? 0 : 10;
var positionElement = document.createElement('span');
positionElement.classList.add('rulez-position-element');
var movePositionElement = function(e) {
positionElement.innerText = positionPrefix + guideConfig.position;
positionElement.style.left = e.pageX + leftPositionMargin + 'px';
positionElement.style.top = e.pageY + topPositionMargin + 'px';
};
var mouseMoveListener = function (e) {
e.preventDefault();
var pos = e[posPropName];
var diff = startPos - pos;
guideConfig.position = startGuidePos - (diff * scale);
if (e.shiftKey) {
// snap to the grid
var halfSnap = c.guideSnapInterval / 2;
var mod = guideConfig.position % c.guideSnapInterval;
var absMod = Math.abs(mod);
var diffSnap = mod;
if (absMod > halfSnap) {
diffSnap = Math.sign(mod) * (c.guideSnapInterval - absMod);
guideConfig.position = Math.round(guideConfig.position + diffSnap);
} else {
guideConfig.position = Math.round(guideConfig.position - diffSnap);
}
} else {
guideConfig.position = Math.round(guideConfig.position);
}
movePositionElement(e);
moveGuide(guide, guideConfig);
};
var mouseUpListener = function (e) {
document.body.classList.remove(globalClassName);
document.body.removeChild(positionElement);
document.removeEventListener('mousemove', mouseMoveListener);
document.removeEventListener('mouseup', mouseUpListener);
};
guide.addEventListener('mousedown', function (e) {
e.stopPropagation();
document.body.classList.add(globalClassName);
startPos = e[posPropName];
startGuidePos = guideConfig.position;
movePositionElement(e);
document.body.appendChild(positionElement);
document.addEventListener('mouseup', mouseUpListener);
document.addEventListener('mousemove', mouseMoveListener);
}, true);
guide.addEventListener('dblclick', function () {
removeGuide(guide, guideConfig);
});
}
function removeGuide(guide, guideConfig) {
guides = guides.filter(function (g) {
return g !== guide;
});
c.guides = c.guides.filter(function (gConfig) {
return gConfig !== guideConfig;
});
guide.parentNode.removeChild(guide);
}
function generateGuide(guideConfig) {
return _createGuideRect(guideConfig);
}
function generateTexts(startPosition, endPosition, elementConfig) {
var texts = [];
for (var i = startPosition; i < endPosition; i += elementConfig.pixelGap) {
var text = createText(i, elementConfig);
g.appendChild(text);
if (elementConfig.renderer) {
elementConfig.renderer(text);
}
texts.push(text);
}
return texts;
}
function createLine(pos, elementConfig) {
switch (elementConfig.type) {
case 'line':
return _createLine(pos, elementConfig);
case 'rect':
return _createRect(pos, elementConfig);
default :
return _createRect(pos, elementConfig);
}
}
function _createGuideRect(guideConfig) {
var guide = _createRectGeneral(0, guideConfig.className, guideConfig.getSize(guideConfig), guideConfig.strokeWidth, 0);
moveGuide(guide, guideConfig);
return guide;
}
function _createLine(pos, elementConfig) {
return _createLineGeneral(pos, elementConfig.className, elementConfig.lineLength, elementConfig.strokeWidth);
}
function _createRect(pos, elementConfig) {
return _createRectGeneral(pos, elementConfig.className, elementConfig.lineLength, elementConfig.strokeWidth);
}
function _createLineGeneral(pos, className, lineLength, strokeWidth) {
var line = window.document.createElementNS(svgNS, 'line');
var defaultAlignment = isDefaultAlignment();
var x1, x2, y1, y2;
if (isVertical()) {
x1 = 'y1';
x2 = 'y2';
y1 = 'x1';
y2 = 'x2';
} else {
x1 = 'x1';
x2 = 'x2';
y1 = 'y1';
y2 = 'y2';
}
line.setAttribute('class', className);
line.setAttribute(x1, addUnits(pos));
line.setAttribute(x2, addUnits(pos));
line.setAttribute(y1, addUnits(defaultAlignment ? '0' : getAlignmentOffset() - lineLength));
line.setAttribute(y2, addUnits(defaultAlignment ? lineLength : getAlignmentOffset()));
line.setAttribute('stroke-width', addUnits(strokeWidth));
return line;
}
function _createRectGeneral(pos, className, lineLength, strokeWidth, alignment) {
var line = window.document.createElementNS(svgNS, 'rect');
var defaultAlignment = isDefaultAlignment();
var x, y, height, width;
if (isVertical()) {
x = 'y';
y = 'x';
height = 'width';
width = 'height';
} else {
x = 'x';
y = 'y';
height = 'height';
width = 'width';
}
var alignmentValue = (typeof alignment !== 'undefined') ? alignment : (defaultAlignment ? '0' : getAlignmentOffset() - lineLength);
line.setAttribute('class', className);
line.setAttribute(x, addUnits(pos));
line.setAttribute(y, addUnits(alignmentValue));
line.setAttribute(height, addUnits(lineLength));
line.setAttribute(width, addUnits(strokeWidth));
return line;
}
function createText(pos, elementConfig) {
var textSvg = window.document.createElementNS(svgNS, 'text');
var yPos = getTextPosY(elementConfig);
var x, y;
textSvg.setAttribute('class', elementConfig.className);
if (isVertical()) {
x = 'y';
y = 'x';
} else {
x = 'x';
y = 'y';
}
textSvg.origPos = pos;
textSvg.origPosAttribute = x;
textSvg.setAttribute(x, addUnits(pos));
textSvg.setAttribute(y, addUnits(yPos));
rotateText(textSvg, elementConfig);
textSvg.textContent = elementConfig.showUnits ? addUnits(pos) : pos;
if (elementConfig.centerText) {
textSvg.setAttribute('text-anchor', 'middle');
}
return textSvg;
}
function createGroup() {
return window.document.createElementNS(svgNS, 'g');
}
function isVertical() {
return c.layout === 'vertical';
}
function isDefaultAlignment() {
return !(c.alignment === 'bottom' || c.alignment === 'right');
}
function getAlignmentOffset() {
return isVertical() ? c.width : c.height;
}
function mergeConfigs(def, cus, notOverrideDef) {
if (!cus) {
return def;
}
for (var param in cus) {
if (cus.hasOwnProperty(param)) {
switch (param) {
case 'divisionDefaults':
case 'textDefaults':
case 'guideDefaults':
mergeConfigs(def[param], cus[param]);
break;
default :
if (!(notOverrideDef && def[param])) {
def[param] = cus[param];
}
}
}
}
if (def.divisions) {
def.divisions.forEach(function (entry) {
mergeConfigs(entry, def.divisionDefaults, entry);
if (!entry.className) {
entry.className = entry.type === 'line' ? 'rulez-line' : 'rulez-rect';
}
});
}
if (def.texts) {
def.texts.forEach(function (entry) {
mergeConfigs(entry, def.textDefaults, entry);
});
}
if (def.guides) {
def.guides.forEach(function (entry) {
mergeConfigs(entry, def.guideDefaults, entry);
});
}
return def;
}
function addUnits(value) {
return value + c.units;
}
function getUnitConversionRate() {
if (c.units === '' || c.units === 'px') {
return 1;
}
var dummyEl = window.document.createElement('div');
dummyEl.style.position = 'absolute';
dummyEl.style.top = '-100000px';
dummyEl.style.left = '-100000px';
dummyEl.style.zIndex = -100000;
dummyEl.style.width = dummyEl.style.height = addUnits(1);
window.document.body.appendChild(dummyEl);
var width = (window.getComputedStyle(dummyEl).width).replace('px', '');
window.document.body.removeChild(dummyEl);
return width;
}
function rotateText(textElement, elementConfig) {
var rotate;
var pos = textElement.origPos;
var yPos = getTextPosY(elementConfig);
if (isVertical()) {
rotate = 'rotate(' + elementConfig.rotation + ' ' + (yPos * unitConversionRate) + ' ' + (pos * unitConversionRate) + ')';
} else {
rotate = 'rotate(' + elementConfig.rotation + ' ' + (pos * unitConversionRate) + ' ' + (yPos * unitConversionRate) + ')';
}
textElement.setAttribute('transform', rotate);
}
function getTextPosY(elementConfig) {
var defaultAlignment = isDefaultAlignment();
return defaultAlignment ? elementConfig.offset : getAlignmentOffset() - elementConfig.offset;
}
};
if (!noGlobal) {
window.Rulez = Rulez;
}
return Rulez;
}));