html5图片弹性布局,HTML5 使用弹性框布局实现可选择和压缩的网格

JavaScript

语言:

JaveScriptBabelCoffeeScript

确定

var Grid = (function() {

'use strict';

var RAF = window.requestAnimationFrame;

var throttle = (function() {

var wait = false;

return function(callback, limit) {

if (!wait) {

callback();

wait = true;

setTimeout(function() {

wait = false;

}, limit);

}

}

})();

function Grid(settings) {

var that = this;

this.settings = $.extend({}, this.defaults, settings);

// calculate a grid's item width & height in percentages

//this.settings.rows = Math.ceil(this.settings.size / this.settings.columns);

this.settings.gridItemW = 100 / this.settings.columns,

this.settings.gridItemH = 100 / this.settings.rows;

this.DOM = {};

}

Grid.prototype = {

defaults: {

rows: 6,

columns: 12,

gap: 6, // in px

areaConstraints: {

minimum: [

[1, 1]

],

maximum: 10

},

areaTemplate: `

×

`

},

init() {

this.DOM.scope = $(this.generate());

this.DOM.items = this.DOM.scope.find('.grid__item');

this.events.binding.call(this);

return this;

},

generate() {

var item = `

grid = `

${Array(this.settings.rows + 2).join(item)}
${Array(this.settings.columns + 2).join(item)}
`;

return grid;

},

getCellCordsByIdx(absIdx) {

// return [(absIdx/this.settings.columns)|0, absIdx % this.settings.columns]; // [column, row]

return [absIdx % this.settings.columns, (absIdx / this.settings.columns) | 0]; // [row, column]

},

clearAllSelected() {

this.DOM.items.filter('.selected').removeClass('selected overlap');

},

selectCells(cells) {

this.clearAllSelected();

var regionOverlap = cells.filter('.defined').length > 0, // check all the cells if any are already "defined" (meanning they belong to an Area)

selectedClass = regionOverlap ? 'selected overlap' : 'selected';

// clear all previously selected cells

cells.addClass(selectedClass);

return !regionOverlap; // no overlap meanning it's a valid region selection

},

getAreaRange(startIdx, endIdx) {

return {

cols: [startIdx[0], endIdx[0]].sort((a, b) => a - b), // left most columns index, right most columns index

rows: [startIdx[1], endIdx[1]].sort((a, b) => a - b) // top most row index, bottom most row index

}

},

/**

* @param {Array} areaData - {rows:[MIN, MAX], cols[MIN, MAX]}

*/

getAreaCssPosition(areaData) {

return {

top: 'calc(' + areaData.rows[0] * this.settings.gridItemH + '%)',

left: 'calc(' + areaData.cols[0] * this.settings.gridItemW + '%)',

height: 'calc(' + (areaData.rows[1] - areaData.rows[0] + 1) * this.settings.gridItemH + '% - ' + this.settings.gap * 2 + 'px)',

width: 'calc(' + (areaData.cols[1] - areaData.cols[0] + 1) * this.settings.gridItemW + '% - ' + this.settings.gap * 2 + 'px)'

}

},

/**

* @param {Array} areaData - {rows:[MIN, MAX], cols[MIN, MAX]}

*/

setAreaProperties(areaElm, areaData) {

// position the template element

areaElm.css(this.getAreaCssPosition(areaData)).data({

'area': areaData

});

return this;

},

/**

* @param {Array} areaData - {rows:[MIN, MAX], cols[MIN, MAX]}

* @param {DOM Object} areaElm - optional - a predefined elmement that will rendered inside the template

*/

renderArea(areaData, areaElm) {

var tmpl = this.settings.areaTemplate;

areaElm = areaElm || '';

if (areaElm)

tmpl.replace('grid__area--empty', ''); // remove the "empty" modifier

areaElm = $(this.settings.areaTemplate).append(areaElm);

if (areaData)

this.setAreaProperties(areaElm, areaData);

return areaElm;

},

removeArea(areaElm) {

areaElm.addClass('removed');

setTimeout(() => {

areaElm.remove()

}, 500)

},

// detects an overlap of a givven area with others

detectOverlap(getMatches) {

var gap = this.settings.gap / 2,

areas = this.DOM.scope.find('> .grid__area:not(.grid__area--dragged)').map(function(idx) {

var rect = this.getBoundingClientRect();

return {

idx: idx,

elm: this,

x: [rect.left - gap, rect.left + rect.width + gap],

y: [rect.top - gap, rect.top + rect.height + gap]

};

}),

matches = [],

i, j;

// check one area against all others

for (i = 0; i < areas.length; i++)

for (j = 0; j < areas.length; j++) {

if (areas[i] !== areas[j] && // avoid checking the same area

areas[i].x[0] < areas[j].x[1] && areas[j].x[0] < areas[i].x[1] && // check X

areas[i].y[0] < areas[j].y[1] && areas[j].y[0] < areas[i].y[1]) { // check Y

if (getMatches)

matches[areas[i].idx] = areas[i].elm;

else

return [areas[i].elm];

}

}

return matches;

},

/**

* Given [x,y] coordinate on the grid, returns the cell (grid item) there

* @param {Array} cords [x, y] point in the whole grid

* @param {Array} gridOffset {top:[value], left:[value]}

*/

getGridPosByCords(cords, gridOffset) {

var xPos = cords.x - gridOffset.left,

xPercentage = xPos / this.DOM.scope[0].clientWidth * 100,

col = Math.floor(xPercentage / this.settings.gridItemW),

yPos = cords.y - gridOffset.top,

yPercentage = yPos / this.DOM.scope[0].clientHeight * 100,

row = Math.floor(yPercentage / this.settings.gridItemH);

// fix overflows

row = Math.min(row, this.settings.rows - 1);

col = Math.min(col, this.settings.columns - 1);

// fix negatives that might happen

row = row < 0 ? 0 : row;

col = col < 0 ? 0 : col;

return [col, row];

},

getItemSizeInPx() {

var someItem = this.DOM.scope.find('.grid__item');

return {

width: someItem.width(),

height: someItem.height()

}

},

events: {

binding() {

this.DOM.scope.on('mousedown.gridItem', this.events.callbacks.onGridItemMouseDown.bind(this))

.on('grid.selected', this.events.callbacks.onSelectedGrid.bind(this))

.on('mousedown', '.resize', this.events.callbacks.onAreaResizeMouseDown.bind(this))

.on('mousedown', '.grid__area__reposition', this.events.callbacks.onAreaRepositionMouseDown.bind(this))

.on('click.removeArea', '.grid__area__removeBtn', this.events.callbacks.onRemoveAreaBtnClick.bind(this))

},

callbacks: {

// Defining an Area

onGridItemMouseDown(e) {

// allow only left mouse click & only on the grid itself (not children)

if (e.which != 1 || !e.target.classList.contains('grid'))

return;

var that = this,

offset = this.DOM.scope.offset(),

startPos = this.getGridPosByCords({

x: e.clientX,

y: e.clientY

}, offset),

areaRange = this.getAreaRange(startPos, startPos),

selectionArea = this.renderArea(areaRange).addClass('grid__area--selection');

// append newly created area to the DOM

this.DOM.scope.append(selectionArea);

// if overlap occurs, remove the selectionArea completely

if (this.detectOverlap().length) {

selectionArea.remove();

return;

}

// bind "mouse move" event

this.DOM.scope.on('mousemove', onGridMouseMove);

$(document).on('mouseup.grid', onGridItemMouseUp.bind(this))

// on mouse-up callbacl

function onGridItemMouseUp(e) {

var overlap = selectionArea.hasClass('grid__area--invalid');

window.getSelection().removeAllRanges(); // fix any text selection (by "releasing" it) that might have ocur on the document

// clear events

$(document).off('mouseup.grid');

this.DOM.scope.off('mousemove');

if (overlap)

this.removeArea(selectionArea);

else

selectionArea.removeClass('grid__area--selection');

}

// on mouse-move callbacl

function onGridMouseMove(e) {

RAF(function() {

var endCellIdx = that.getGridPosByCords({

x: e.clientX,

y: e.clientY

}, offset);

areaRange = that.getAreaRange(startPos, endCellIdx);

// validSelection = this.selectCells(selectionCells.cells); // try to select the cells (if not overlapping occured)

// detect *any* overlap that might exist and set a class if so

selectionArea.toggleClass('grid__area--invalid', !!that.detectOverlap().length);

that.setAreaProperties(selectionArea, areaRange);

})

}

},

// Resizing

onAreaResizeMouseDown(mouseDownEvent) {

if (mouseDownEvent.which != 1) return;

var that = this,

resizeElm = $(mouseDownEvent.target), // the side-corner that was clicked on

areaItem = resizeElm.closest('.grid__area'), // area item

dir = mouseDownEvent.target.className.split('--')[1], // direction where dragging it allowed

boxArea = areaItem.data('area'), // get current area position

newBoxArea = $.extend(true, {}, boxArea), // new box area starts as the currently defined area position

lastValidRange,

mouseup = false, // flag on mouse up

minPosition = this.getAreaCssPosition({

cols: [0, 0],

rows: [0, 0]

}),

offset = this.DOM.scope.offset(); // grid offset cords

areaItem.addClass('grid__area--resized');

resizeElm.addClass('active');

this.DOM.scope.on('mousemove.resize', onResizeMouseMove);

// on mouse-up

$(document).on('mouseup.grid', function() {

mouseup = true;

window.getSelection().removeAllRanges(); // clear any window text selection

resizeElm.removeClass('active');

$(document).off('mouseup.grid');

that.DOM.scope.off('mousemove.resize');

var overlap = !!that.detectOverlap().length;

if (lastValidRange)

that.setAreaProperties(areaItem, lastValidRange);

areaItem.removeClass('grid__area--invalid grid__area--resized');

});

// on mouse-move

function onResizeMouseMove(e) {

RAF(function() {

if (mouseup) return;

var pos = that.getGridPosByCords({

x: e.clientX,

y: e.clientY

}, offset),

overlap,

tempPos,

areaRange;

// check constraints before updating positions

if (dir == 'top' && pos[1] <= boxArea.rows[1]) { // as long as Y position is lower or equal the original END Y position of the area

newBoxArea.rows[0] = pos[1];

if (newBoxArea.rows[1] == newBoxArea.rows[0]) {

areaItem[0].style.top = 'calc(' + newBoxArea.rows[0] * that.settings.gridItemH + '% + ' + that.settings.gap + 'px)';

areaItem[0].style.height = minPosition.height;

} else {

areaItem[0].style.top = e.clientY - offset.top - mouseDownEvent.offsetY - 2 + 'px';

areaItem[0].style.height = 'calc(' + (boxArea.rows[1] - boxArea.rows[0] + 1) * that.settings.gridItemH + '% + ' + (mouseDownEvent.clientY - e.clientY - that.settings.gap * 3) + 'px)';

}

}

if (dir == 'bottom' && pos[1] >= boxArea.rows[0]) { // as long as Y position is higher or equal the original START Y position of the area

newBoxArea.rows[1] = pos[1];

if (newBoxArea.rows[1] == newBoxArea.rows[0])

areaItem[0].style.height = minPosition.height;

else

areaItem[0].style.height = 'calc(' + (boxArea.rows[1] - boxArea.rows[0] + 1) * that.settings.gridItemH + '% + ' + (e.clientY - mouseDownEvent.clientY - that.settings.gap * 2) + 'px)';

}

if (dir == 'left' && pos[0] <= boxArea.cols[1]) { // as long as X position is lower or equal the original END X position of the area

newBoxArea.cols[0] = pos[0];

if (newBoxArea.cols[1] == newBoxArea.cols[0]) {

areaItem[0].style.left = 'calc(' + newBoxArea.cols[0] * that.settings.gridItemW + '% + ' + that.settings.gap + 'px)';

areaItem[0].style.width = minPosition.width;

} else {

areaItem[0].style.left = e.clientX - offset.left - mouseDownEvent.offsetX - 2 + 'px';

areaItem[0].style.width = 'calc(' + (boxArea.cols[1] - boxArea.cols[0] + 1) * that.settings.gridItemW + '% + ' + (mouseDownEvent.clientX - e.clientX - that.settings.gap * 3) + 'px)';

}

}

if (dir == 'right' && pos[0] >= boxArea.cols[0]) { // as long as X position is higher or equal the original START X position of the area

newBoxArea.cols[1] = pos[0];

if (newBoxArea.cols[1] == newBoxArea.cols[0])

areaItem[0].style.width = minPosition.width;

else

areaItem[0].style.width = 'calc(' + (boxArea.cols[1] - boxArea.cols[0] + 1) * that.settings.gridItemW + '% + ' + (e.clientX - mouseDownEvent.clientX - that.settings.gap * 2) + 'px)';

}

// that.setAreaProperties(areaItem, areaRange);

throttle(function() {

if (mouseup) return;

overlap = !!that.detectOverlap().length;

areaItem.toggleClass('grid__area--invalid', overlap);

if (!overlap) {

lastValidRange = that.getAreaRange([newBoxArea.cols[0], newBoxArea.rows[0]], [newBoxArea.cols[1], newBoxArea.rows[1]]);

}

}, 80);

})

}

},

// Re-positioning (via drag)

onAreaRepositionMouseDown(mouseDownEvent) {

if (mouseDownEvent.which != 1) return;

var that = this,

offset = this.DOM.scope.offset(),

areaItem = $(mouseDownEvent.target).closest('.grid__area'),

areaItemClone = $('

').attr('style', areaItem.attr('style')), // create a clone of the area with the same style (position)

boxArea = areaItem.data('area'),

newBoxArea = {

cols: 1,

rows: 1

},

lastAreaCords = [boxArea.cols[0], boxArea.rows[0]],

destPos,

areaSize = {

x: boxArea.cols[1] - boxArea.cols[0] + 1,

y: boxArea.rows[1] - boxArea.rows[0] + 1

},

mouseup = false;

areaItem.addClass('grid__area--dragged');

that.DOM.scope.addClass('dragging')

that.DOM.scope.append(areaItemClone);

// Mouse move & up Events

this.DOM.scope.on('mousemove.reposition', onAreaRepositionMouseMove);

$(document).on('mouseup.grid', onMouseUp);

// mouse up callback

function onMouseUp(e) {

mouseup = true;

$(document).off('mouseup.grid');

window.getSelection().removeAllRanges(); // fix any text selection (by "releasing" it) that might have ocur on the document

areaItem.removeClass('grid__area--dragged');

that.DOM.scope.removeClass('dragging').off('mousemove.reposition');

var selectionCells = that.getAreaRange(lastAreaCords, [lastAreaCords[0] + areaSize.x - 1, lastAreaCords[1] + areaSize.y - 1]),

overlap = areaItem.hasClass('grid__area--invalid'); // try to select the cells (if not overlapping occured)

areaItem.removeClass('grid__area--invalid');

if (overlap) {

areaItemClone.remove();

areaItem.addClass('grid__area--snapBack');

RAF(function() {

areaItem.css(that.getAreaCssPosition(boxArea));

// areaItem.css({ left:boxArea.cols[0] * that.settings.gridItemW + '%', top:boxArea.rows[0] * that.settings.gridItemH + '%' });

});

setTimeout(function() {

areaItem.removeClass('grid__area--snapBack')

}, 500);

} else {

RAF(function() {

that.setAreaProperties(areaItem, selectionCells);

});

setTimeout(function() {

areaItemClone.remove()

}, 300);

}

}

// mouse move callback

function onAreaRepositionMouseMove(e) {

RAF(function() {

if (mouseup) return; // make sure nothing will happen after "mouseup" was invoked

destPos = that.getGridPosByCords({

x: e.clientX,

y: e.clientY

}, offset);

var vaildColumnCords = destPos[0] + areaSize.x > that.settings.columns,

vaildRowCords = destPos[1] + areaSize.y > that.settings.rows,

xExceedsRight = areaItem[0].clientWidth + e.clientX - mouseDownEvent.offsetX > that.DOM.scope[0].clientWidth + offset.left,

yExceedsBottom = areaItem[0].clientHeight + e.clientY - mouseDownEvent.offsetY > that.DOM.scope[0].clientHeight + offset.top,

yExceedsTop = e.clientY - mouseDownEvent.offsetY - offset.top < 0,

areaPos = {

left: e.clientX - offset.left - mouseDownEvent.offsetX - 8 + 'px',

top: e.clientY - offset.top - mouseDownEvent.offsetY - 8 + 'px'

},

overlap = !!that.detectOverlap().length;

//

// position the "real" area

areaItem[0].style.left = areaPos.left;

areaItem[0].style.top = areaPos.top;

// detect *any* overlap that might exist and set a class if so

areaItem.toggleClass('grid__area--invalid', overlap);

// return the rummy to it's preior position

// areaItemClone.addClass('grid__area--snapBack').css( that.getAreaCssPosition(boxArea) );

areaItemClone.removeClass('grid__area--snapBack')

if (vaildColumnCords)

destPos[0] = that.settings.columns - areaSize.x;

if (vaildRowCords)

destPos[1] = that.settings.rows - areaSize.y;

// don't update clone styles if no change is needed

if (destPos[0] != lastAreaCords[0] || destPos[1] != lastAreaCords[1]) {

// snap the clone to allowed grid items

areaItemClone[0].style.left = 'calc(' + destPos[0] * that.settings.gridItemW + '%)';

areaItemClone[0].style.top = 'calc(' + destPos[1] * that.settings.gridItemH + '%)';

lastAreaCords = destPos;

}

if (overlap)

lastAreaCords = boxArea;

// freelly move the area itself (within the allowed grid)

// if( xExceedsRight )

// areaPos.left = that.DOM.scope[0].clientWidth - areaItem[0].clientWidth + 'px';

// if( yExceedsBottom )

// areaPos.top = that.DOM.scope[0].clientHeight - areaItem[0].clientHeight + 'px';

// if( yExceedsTop )

// areaPos.top = '0px';

});

}

},

// When an Area has been defined

onSelectedGrid(e, selectedGrid) {

// this.clearAllSelected();

// this.renderArea(selectedGrid);

},

// Removing (deleting) an Area

onRemoveAreaBtnClick(e) {

var areaElm = $(e.currentTarget).closest('.grid__area').addClass('removed');

this.removeArea(areaElm);

}

}

}

}

return Grid;

})();

//

$.when($.get("https://npmcdn.com/packery@2.0/dist/packery.pkgd.min.js"));

var grid = new Grid().init(),

packery;

$(document.body).append(grid.DOM.scope.addClass('editMode'));

///

// events

$('.pack').on('click', onPackBtnClick);

$('.clearAll').on('click', onClearAllBtnClick);

function onPackBtnClick() {

if (packery) {

packery.packery('reloadItems');

packery.packery('layout');

} else {

packery = grid.DOM.scope.packery({

itemSelector: '.grid__area',

resizeContainer: false,

percentPosition: true,

resize: false,

gutter: 1,

containerStyle: null

}).on('layoutComplete', function() {});

}

}

function onClearAllBtnClick() {

grid.DOM.scope.find('.grid__area').remove();

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值