/* 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.>} */ var texts = []; /** * Array of all guides * @type {Array.} */ 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.round((startTextPos + (j - additionalDivisionsAmount * amountPerMaxDistance) * textConfig.pixelGap) * scale *100)/100; 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; }));