From 8875b53d86aee7b46513f6292cec9387c88d306e Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 11 May 2016 03:25:39 +0200 Subject: [PATCH] Start Utils with sorter --- src/config/require-config.js | 3 +- src/style_manager/templates/layer.html | 3 + src/style_manager/view/LayerView.js | 10 + src/utils/Sorter.js | 382 +++++++++++++++++++++ src/utils/main.js | 13 + styles/css/main.css | 11 + styles/scss/main.scss | 11 + test/runner/main.js | 3 +- test/specs/style_manager/view/LayerView.js | 1 - test/specs/utils/Sorter.js | 163 +++++++++ test/specs/utils/main.js | 17 + 11 files changed, 614 insertions(+), 3 deletions(-) create mode 100644 src/utils/Sorter.js create mode 100644 src/utils/main.js create mode 100644 test/specs/utils/Sorter.js create mode 100644 test/specs/utils/main.js diff --git a/src/config/require-config.js b/src/config/require-config.js index 3d2836551..9d4656c78 100644 --- a/src/config/require-config.js +++ b/src/config/require-config.js @@ -45,6 +45,7 @@ require.config({ { name: 'CssComposer', location: 'css_composer', }, { name: 'Commands', location: 'commands', }, { name: 'Canvas', location: 'canvas', }, - { name: 'Panels', location: 'panels', } + { name: 'Panels', location: 'panels', }, + { name: 'Utils', location: 'utils', } ] }); \ No newline at end of file diff --git a/src/style_manager/templates/layer.html b/src/style_manager/templates/layer.html index 58a55fb47..e045fb7ac 100644 --- a/src/style_manager/templates/layer.html +++ b/src/style_manager/templates/layer.html @@ -1,3 +1,6 @@ +
+ +
<%= label %>
diff --git a/src/style_manager/view/LayerView.js b/src/style_manager/view/LayerView.js index 873183362..d745009d7 100644 --- a/src/style_manager/view/LayerView.js +++ b/src/style_manager/view/LayerView.js @@ -20,6 +20,7 @@ define(['backbone', 'text!./../templates/layer.html'], this.listenTo(this.model, 'change:value', this.valueChanged); this.listenTo(this.model, 'change:props', this.showProps); this.events['click #' + this.pfx + 'close-layer'] = 'remove'; + //this.events['mousedown > #' + this.pfx + 'move'] = 'initSorter'; if( !this.model.get('preview') ){ this.$el.addClass(this.pfx + 'no-preview'); @@ -28,6 +29,15 @@ define(['backbone', 'text!./../templates/layer.html'], this.delegateEvents(); }, + /** + * Delegate sorting + * @param {Event} e + * */ + initSorter: function(e){ + if(this.sorter) + this.sorter.startMove(this); + }, + /** * Returns properties * @return {Collection|null} diff --git a/src/utils/Sorter.js b/src/utils/Sorter.js new file mode 100644 index 000000000..42af3065d --- /dev/null +++ b/src/utils/Sorter.js @@ -0,0 +1,382 @@ +define(['backbone'], + function(Backbone) { + + return Backbone.View.extend({ + + initialize: function(opt) { + _.bindAll(this,'startSort','onMove','endMove','rollback', 'itemLeft'); + var o = opt || {}; + this.config = o.config || {}; + this.pfx = this.config.stylePrefix || ''; + this.itemClass = '.' + this.pfx + this.config.itemClass; + this.itemsClass = '.' + this.pfx + this.config.itemsClass; + this.setElement('.'+this.pfx+this.config.containerId); + + this.elT = 0; + this.elL = 0; + + this.el = document.querySelector(o.container); + this.$el = $(this.el); + this.containerSel = 'div'; + this.itemSel = 'div'; + }, + + /** + * Returns true if the element matches with selector + * @param {Element} el + * @param {String} selector + * @return {Boolean} + */ + matches: function(el, selector){ + var els = (el.parentNode || document.body).querySelectorAll(selector); + var i = 0; + while (els[i] && els[i] !== el) + ++i; + return !!els[i]; + }, + + /** + * Closest parent + * @param {Element} el + * @param {String} selector + * @return {Element|null} + */ + closest: function(el, selector){ + var elem = el.parentNode; + while (elem && elem.nodeType === 1) { + if (this.matches(elem, selector)) + return elem; + elem = elem.parentNode; + } + return null; + }, + + /** + * Get the offset of the element + * @param {HTMLElement} el + * @return {Object} + */ + offset: function(el){ + var rect = el.getBoundingClientRect(); + return { + top: rect.top + document.body.scrollTop, + left: rect.left + document.body.scrollLeft + }; + }, + + /** + * Create placeholder + * @return {HTMLElement} + */ + createPlaceholder: function(){ + var pfx = this.pfx; + var el = document.createElement('div'); + var ins = document.createElement('div'); + el.id = pfx + 'placeholder'; + el.style.display = 'none'; + el.style['pointer-events'] = 'none'; + ins.id = pfx + "plh-int"; + el.appendChild(ins); + return el; + }, + + /** + * Picking component to move + * @param {Object} Element view + * */ + startSort: function(el){ + this.moved = 0; + this.eV = el.el || el; + + // Create placeholder if not exists + if(!this.plh){ + this.plh = this.createPlaceholder(); + this.el.appendChild(this.plh); + } + //freeze el + + this.$el.on('mousemove',this.onMove); + $(document).on('mouseup',this.endMove); + $(document).on('keypress',this.rollback); + }, + + /** + * During move + * @param {Event} e + * */ + onMove: function(e){ + this.moved = 1; + + // Turn placeholder visibile + var plh = this.plh; + if(plh.style.display === 'none'){ + plh.style.display = 'block'; + } + + // Cache all necessary positions + var rect = this.el.getBoundingClientRect(); + var body = document.body; + var eO = { top: rect.top + body.scrollTop, left: rect.left + body.scrollLeft }; + this.elT = eO.top; + this.elL = eO.left; + this.rY = (e.pageY - this.elT) + this.el.scrollTop; + this.rX = (e.pageX - this.elL) + this.el.scrollLeft; + + this.inspect(e); + this.updatePosition(this.rX, this.rY); + var actualPos = this.posIndex+':'+this.posMethod; + + //If there is a significant changes with the pointer + if(!this.lastPos || (this.lastPos != actualPos)){ + this.updatePlaceholderPos(this.posIndex, this.posMethod); + this.lastPos = this.posIndex+':'+this.posMethod; + } + //Working alternative for find taget element + //var $targetEl = this.$selParent.children('.'+this.pfx+this.config.itemClass).eq(this.aIndex); + }, + + /** + * Get children dimensions + * @param {HTMLELement} el Element root + * @retun {Array} + * */ + getChildrenDim: function(elem){ + var dim = []; + var ch = elem.children;//TODO filter match + for (var i = 0, len = ch.length; i < len; i++) { + var el = ch[i]; + var elO = this.offset(el); + dim.push([elO.top - this.elT, elO.left - this.elL, el.offsetHeight, el.offsetWidth, true, el]); + } + return dim; + }, + + /** + * Search where to put placeholder + * @param int X position of the mouse + * @param int Y position of the mouse + * @retun void + * */ + updatePosition: function( posX, posY ){ + this.posMethod = "before"; + this.posIndex = 0; + var leftLimit = 0, xLimit = 0, dimRight = 0, yLimit = 0, xCenter = 0, yCenter = 0, dimDown = 0, dim = 0; + for(var i = 0; i < this.cDim.length; i++){ //Dim => t,l,h,w + dim = this.cDim[i]; + dimDown = dim[0] + dim[2]; + yCenter = dim[0] + (dim[2] / 2); //Horizontal center + xCenter = dim[1] + (dim[3] / 2); //Vertical center + dimRight = dim[1] + dim[3]; + if( (xLimit && dim[1] > xLimit) || (yLimit && yCenter > yLimit) || + (leftLimit && dimRight < leftLimit)) //No need with this one if over the limit + continue; + if(!dim[4]){ //If it's not inFlow (like float element) + if( posY < dimDown) + yLimit = dimDown; + if( posX < xCenter){ //If mouse lefter than center + xLimit = xCenter; + this.posMethod = "before"; + }else{ + leftLimit = xCenter; + this.posMethod = "after"; + } + this.posIndex = i; + }else{ + this.posIndex = this.aIndex = i; + if( posY < yCenter ){ //If mouse upper than center + this.posMethod = "before"; //Should place helper before + if(posY < dim[0]) + this.aIndex = i - 1; + break; //No need to continue under inFlow element + }else + this.posMethod = "after"; + } + } + }, + + /** + * Updates the position of the placeholder + * @param int Index of the nearest child + * @param str Before or after position + * @return void + * */ + updatePlaceholderPos: function(index, method){ + var marg = 0, t = 0, l = 0, w = 0, h = 0, + un = 'px', + margI = 5, + plh = this.$plh[0]; + if( this.cDim[index] ){ + var elDim = this.cDim[index]; + //If it's like with 'float' style + if(!elDim[4]){ + w = 'auto'; + h = elDim[2] - (marg * 2) + un; + t = elDim[0] + marg; + l = (method == 'before') ? (elDim[1] - marg) : (elDim[1] + elDim[3] - marg); + }else{ + //w = '100%'; + w = elDim[3] + un; + //h = elDim[3] + un; + t = (method == 'before') ? (elDim[0] - marg) : (elDim[0] + elDim[2] - marg); + l = elDim[1]; + } + }else{ + if(this.$targetEl){ + var trg = this.$targetEl[0], + $elO = this.$targetEl.offset(); + t = $elO.top - this.elT + margI + 17; + l = $elO.left - this.elL + margI * 7; + w = (parseInt(trg.offsetWidth) - margI * 14) + un; + } + } + plh.style.top = t + un; + plh.style.left = l + un; + if(w) + plh.style.width = w; + if(h) + plh.style.height = h; + }, + + /** + * Leave item + * @param event + * + * @return void + * */ + endMove: function(e){ + this.$el.off('mousemove',this.onMove); + $(document).off('mouseup', this.endMove); + $(document).off('keypress', this.rollback); + this.eV.unfreeze(); + this.$plh.hide(); + if(this.moved) + this.move(this.$targetEl, this.$sel, this.posIndex, this.posMethod); + this.itemLeft(); + }, + + /** + * Move component to new position + * @param {Object} Component to move + * @param {Object} Target component + * @param {Integer} Indicates the position inside the collection + * @param {String} Before of after component + * + * @return void + * */ + move: function(target, el, posIndex, method){ + var trg = target|| this.$targetEl; + trg = trg || this.$backupEl; + if(!trg) + return; + var index = posIndex || 0; + var model = el.data("model"); + var collection = model.collection; + var targetModel = trg.data('model'); + var targetCollection = targetModel.collection; + + if(!this.cDim.length) + targetCollection = targetModel.get('components'); + + if(targetCollection && targetModel.get('droppable')){ + index = method == 'after' ? index + 1 : index; + var modelTemp = targetCollection.add({style:{}}, { at: index}); + var modelRemoved = collection.remove(model, { silent:false }); + targetCollection.add(modelRemoved, { at: index, silent:false }); + targetCollection.remove(modelTemp); + }else + console.warn("Invalid target position"); + }, + + /** + * Track inside which element pointer entered + * @param event + * + * @return void + * */ + inspect: function(e){ + var item = $(e.target).closest(this.itemClass); + if(!this.$targetEl || (item.length && item[0] != this.$targetEl[0]) ){ + this.status = 1; + if(item.length){ + this.$targetEl = this.$backupEl = item; + this.$targetElP = this.$targetEl.parent(); + this.$targetsEl = this.$targetEl.find(this.itemsClass + ':first'); + this.$targetEl.on('mouseleave', this.itemLeft); + this.targetM = this.$targetEl.data('model'); + this.dimT = this.getTargetDim(this.$targetEl[0]); + this.cDim = this.getChildrenDim(); + } + }else if( this.nearToBorders(this.$targetEl[0]) || this.$targetEl[0] == this.$sel[0] ){ + if(this.status == 1){ + this.status = 2; + this.lastPos = null; + this.cDim = this.getChildrenDim(this.$targetElP); + } + }else if( !this.nearToBorders(this.$targetEl[0]) ){ + if(this.status == 2){ + this.status = 1; + this.lastPos = null; + } + this.cDim = []; + } + }, + + /** + * Triggered when pointer leaves item + * @param event + * + * @return void + * */ + itemLeft: function(e){ + if(this.$targetEl){ + this.$targetEl.off('mouseleave',this.itemLeft); + this.$targetEl = null; + } + }, + + /** + * Returns dimension of the target + * @param Event + * + * @return Array + * */ + getTargetDim: function(e){ + var $el = $(e), + $elO = $el.offset(); + return [ $elO.top - this.elT, $elO.left - this.elL, $el.outerHeight(), $el.outerWidth() ]; + }, + + /** + * Check if pointer is near to the borders of the target + * @param event + * @return Bool + * */ + nearToBorders: function(e){ + var m = 10; //Limit in pixels for be near + if(!this.dimT) + return; + var dimT = this.dimT; + if( ((dimT[0] + m) > this.rY) || (this.rY > (dimT[0] + dimT[2] - m)) || + ((dimT[1] + m) > this.rX) || (this.rX > (dimT[1] + dimT[3] - m)) ) + return 1; + else + return 0; + }, + + /** + * Rollback to previous situation + * @param Event + * @param Bool Indicates if rollback in anycase + * @return void + * */ + rollback: function(e, force){ + var key = e.which || e.keyCode; + if(key == 27 || force){ + this.moved = false; + this.endMove(); + } + return; + }, + + }); +}); \ No newline at end of file diff --git a/src/utils/main.js b/src/utils/main.js new file mode 100644 index 000000000..b23bc9054 --- /dev/null +++ b/src/utils/main.js @@ -0,0 +1,13 @@ +define(function(require) { + + var Utils = function(){ + + var Sorter = require('./Sorter'); + + return { + Sorter: Sorter, + }; + }; + + return Utils; +}); \ No newline at end of file diff --git a/styles/css/main.css b/styles/css/main.css index e1faf3a04..1242ef154 100644 --- a/styles/css/main.css +++ b/styles/css/main.css @@ -3215,6 +3215,17 @@ ol.example li.placeholder:before { .wte-sm-sector .wte-sm-btn-upload #wte-sm-label, .wte-clm-tags .wte-sm-btn-upload #wte-sm-label { padding: 2px 0; } +.wte-sm-layer > #wte-sm-move { + opacity: 0.3; + filter: alpha(opacity=30); + cursor: pointer; + font-size: 12px; + float: left; + margin: 0 5px 0 0; } + .wte-sm-layer > #wte-sm-move:hover { + opacity: 0.5; + filter: alpha(opacity=50); } + /********* END Style Manager **********/ /********* Class manager **********/ .wte-clm-tags { diff --git a/styles/scss/main.scss b/styles/scss/main.scss index a01c0aea9..7374cb650 100644 --- a/styles/scss/main.scss +++ b/styles/scss/main.scss @@ -740,6 +740,17 @@ $arrowColor: darken($fontColor,24%); /*b1b1b1*/ .#{$sm-prefix}btn-upload ##{$sm-prefix}label { padding: 2px 0;} } +.#{$sm-prefix}layer > ##{$sm-prefix}move { + @include opacity(0.3); + cursor: pointer; + font-size: 12px; + float: left; + margin: 0 5px 0 0; + + &:hover{ + @include opacity(0.5); + } +} /********* END Style Manager **********/ /********* Class manager **********/ diff --git a/test/runner/main.js b/test/runner/main.js index 28bf7e8df..f0e519ceb 100644 --- a/test/runner/main.js +++ b/test/runner/main.js @@ -17,7 +17,8 @@ require(['../src/config/require-config.js', 'config/config.js'], function() { 'specs/code_manager/main.js', 'specs/panels/main.js', 'specs/commands/main.js', - 'specs/style_manager/main.js' + 'specs/style_manager/main.js', + 'specs/utils/main.js' ], function(chai) { var should = chai.should(), diff --git a/test/specs/style_manager/view/LayerView.js b/test/specs/style_manager/view/LayerView.js index 0af7c4679..f7aa3dc21 100644 --- a/test/specs/style_manager/view/LayerView.js +++ b/test/specs/style_manager/view/LayerView.js @@ -80,7 +80,6 @@ define([path + 'LayerView', 'StyleManager/model/Layers'], (view.model.get('props') === null).should.equal(true); }); - }); } }; diff --git a/test/specs/utils/Sorter.js b/test/specs/utils/Sorter.js new file mode 100644 index 000000000..0151fa906 --- /dev/null +++ b/test/specs/utils/Sorter.js @@ -0,0 +1,163 @@ +var path = 'Utils/'; +define([path + 'Sorter',], + function(Sorter) { + + return { + run : function(){ + + describe('Sorter', function() { + var obj; + var parent; + + beforeEach(function () { + parent = document.createElement('div'); + parent.setAttribute('class', 'parent1'); + document.body.appendChild(parent); + obj = new Sorter({container: '.parent1'}); + }); + + afterEach(function () { + delete obj; + }); + + it('matches class', function() { + var el = document.createElement('div'); + el.setAttribute('class', 'test test2'); + parent.appendChild(el); + obj.matches(el, '.test').should.equal(true); + obj.matches(el, '.test2').should.equal(true); + obj.matches(el, '.test3').should.equal(false); + }); + + it('matches id', function() { + var el = document.createElement('div'); + el.setAttribute('id', 'test2'); + parent.appendChild(el); + obj.matches(el, '#test2').should.equal(true); + obj.matches(el, '.test2').should.equal(false); + obj.matches(el, '#test').should.equal(false); + }); + + it('matches tag', function() { + var el = document.createElement('span'); + parent.appendChild(el); + obj.matches(el, 'span').should.equal(true); + obj.matches(el, 'div').should.equal(false); + obj.matches(el, '*').should.equal(true); + }); + + it('Creates placeholder', function() { + obj.createPlaceholder().id.should.equal('placeholder'); + }); + + describe('Closest method', function() { + var parent2; + var parent3; + + beforeEach(function () { + parent2 = document.createElement('span'); + parent2.setAttribute('class', 'parent2'); + parent3 = document.createElement('div'); + parent3.setAttribute('class', 'parent3'); + parent.appendChild(parent2); + parent2.appendChild(parent3); + }); + + it('Closest by class', function() { + var el = document.createElement('div'); + parent3.appendChild(el); + obj.closest(el, '.parent2').should.deep.equal(parent2); + obj.closest(el, '.parent3').should.deep.equal(parent3); + obj.closest(el, '.parent1').should.deep.equal(parent); + }); + + it('Closest by tag', function() { + var el = document.createElement('div'); + el.setAttribute('class', 'el'); + parent3.appendChild(el); + obj.closest(el, 'span').should.deep.equal(parent2); + obj.closest(el, 'div').should.deep.equal(parent3); + obj.closest(el, '*').should.deep.equal(parent3); + }); + + }); + + describe('With elements', function() { + + var parent2; + var parent3; + var sib1; + var sib2; + var sib3; + var sib4; + var el; + + beforeEach(function () { + parent2 = document.createElement('span'); + parent2.setAttribute('class', 'parent2'); + parent3 = document.createElement('div'); + parent3.setAttribute('class', 'parent3'); + parent.appendChild(parent2); + parent2.appendChild(parent3); + el = document.createElement('div'); + el.setAttribute('class', 'el'); + parent3.appendChild(el); + + sib1 = document.createElement('div'); + sib2 = document.createElement('div'); + sib3 = document.createElement('div'); + sib4 = document.createElement('div'); + sib1.style.width = '100px'; + sib1.style.height = '50px'; + sib2.style.width = '100px'; + sib2.style.height = '50px'; + sib3.style.width = '100px'; + sib3.style.height = '50px'; + sib3.style.float = 'left'; + sib4.style.width = '70px'; + sib4.style.height = '50px'; + sib4.style.float = 'left'; + el.appendChild(sib1); + el.appendChild(sib2); + el.appendChild(sib3); + el.appendChild(sib4); + }); + + it('startSort inits correctly inits', function() { + obj.startSort(el); + obj.plh.style.display.should.equal('none'); + }); + + it.skip('onMove', function() { + obj.startSort(el); + obj.onMove({ + pageX: 0, + pageY: 0, + }); + obj.plh.style.display.should.equal('block'); + }); + + it('getChildrenDim from element', function() { + el.style.position = 'absolute'; + el.style.top = '0'; + var ch = obj.getChildrenDim(el); + ch = ch.map(function(v){ + return v.slice(0, 5); + }); + var result = [ + [0, 0, 50, 100, true], + [50, 0, 50, 100, true], + [100, 0, 50, 100, true], + [100, 100, 50, 70, true], + ] + ch.should.deep.equal(result); + }); + + }); + + }); + + } + }; + +}); \ No newline at end of file diff --git a/test/specs/utils/main.js b/test/specs/utils/main.js new file mode 100644 index 000000000..175dc100a --- /dev/null +++ b/test/specs/utils/main.js @@ -0,0 +1,17 @@ +var modulePath = './../../../test/specs/utils'; + +define([ + 'Utils', + modulePath + '/Sorter' + ], + function( + Utils, + Sorter + ) { + + describe('Utils', function() { + + Sorter.run(); + + }); +}); \ No newline at end of file