client/app/pods/components/window-grid/component.js
//
// Copyright 2009-2014 Ilkka Oksanen <iao@iki.fi>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an "AS
// IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language
// governing permissions and limitations under the License.
//
/* globals $ */
import { autorun } from 'mobx';
import { A } from '@ember/array';
import { next, scheduleOnce, bind } from '@ember/runloop';
import { observer } from '@ember/object';
import Component from '@ember/component';
import alertStore from '../../../stores/AlertStore';
import settingStore from '../../../stores/SettingStore';
import windowStore from '../../../stores/WindowStore';
import { dispatch } from '../../../utils/dispatcher';
const CURSORWIDTH = 50;
export default Component.extend({
init() {
this._super();
this.set('windowComponents', A([]));
this.disposers = [
autorun(() => this.set('theme', settingStore.settings.theme)),
autorun(() => this.set('emailConfirmed', settingStore.settings.emailConfirmed)),
autorun(() => this.set('initDone', windowStore.initDone)),
autorun(() => this.set('windowIds', Array.from(windowStore.windows.keys()))),
autorun(() => this.set('alerts', alertStore.alerts))
];
},
didDestroyElement() {
this.disposers.forEach(element => element());
},
classNames: ['grid', 'flex-1', 'flex-grow-column'],
dimensions: null,
cursor: {},
movingWindow: null,
relayoutScheduled: false,
relayoutAnimate: null,
windowComponents: null,
mustRelayout: observer('initDone', 'theme', function () {
next(this, function () {
this._layoutWindows(false);
});
}),
mustRelayoutAfterRender: observer('alerts', 'emailConfirmed', function () {
scheduleOnce('afterRender', this, function () {
this._layoutWindows(false);
});
}),
actions: {
joinLobby() {
dispatch('JOIN_GROUP', {
name: 'lobby',
network: 'MAS',
password: '',
acceptCb: () => {},
rejectCb: () => {}
});
},
relayoutAfterRender(options) {
scheduleOnce('afterRender', this, function () {
this.send('relayout', options);
});
},
relayout(options) {
if (!this.initDone) {
return;
}
// If there's at least one relayout call without animation, it takes precedence.
if (this.relayoutAnimate === null || this.relayoutAnimate === true) {
this.relayoutAnimate = options.animate;
}
if (this.relayoutScheduled) {
return;
}
this.relayoutScheduled = true;
next(this, function () {
this.relayoutScheduled = false;
this._layoutWindows(this.relayoutAnimate);
this.relayoutAnimate = null;
});
},
dragWindowStart(discussionWindow, event) {
this._dragWindowStart(discussionWindow, event);
},
registerWindow(discussionWindow) {
this.windowComponents.addObject(discussionWindow);
},
unregisterWindow(discussionWindow) {
this.windowComponents.removeObject(discussionWindow);
}
},
didInsertElement() {
$(window).on(
'resize',
bind(this, function () {
this._layoutWindows(false);
})
);
},
_dragWindowStart(discussionWindow, event) {
this.movingWindow = discussionWindow;
this.set('draggedWindow', discussionWindow);
this.movingWindow.$().addClass('moving').css('z-index', 200);
$('#window-cursor').show();
this._dragWindow(event);
const handleDragMove = dragMoveEvent => this._dragWindow(dragMoveEvent);
const handleDragEnd = dragEndEvent => {
this._dragWindowEnd(dragEndEvent);
document.removeEventListener('mousemove', handleDragMove, false);
document.removeEventListener('mouseup', handleDragEnd, false);
};
document.addEventListener('mousemove', handleDragMove, false);
document.addEventListener('mouseup', handleDragEnd, false);
},
_dragWindow(event) {
const cursor = this._calculateCursorPosition(event);
if (cursor.x === null && this.cursor.x === null) {
// Still outside of grid
return;
}
if (
this.cursor.x !== cursor.x ||
this.cursor.y !== cursor.y ||
(cursor.section !== 'middle' && cursor.section !== this.cursor.section)
) {
this.cursor = cursor;
this._drawCursor(cursor);
this.dimensions.forEach((row, rowIndex) => {
row.forEach((masWindow, columnIndex) => {
this._markWindow(masWindow, columnIndex, rowIndex, cursor);
});
});
this._animate(200);
}
event.preventDefault();
},
_dragWindowEnd(event) {
const cursor = this.cursor;
this.set('draggedWindow', false);
this.movingWindow.$().removeClass('moving').css('z-index', '');
$('#window-cursor').hide();
const desktop = $(event.target).data('desktop-id');
if (typeof desktop !== 'undefined') {
const desktopId = desktop === 'new' ? Math.floor(new Date() / 1000) : parseInt(desktop);
dispatch('MOVE_WINDOW', {
column: 0,
row: 0,
desktop: desktopId,
windowId: this.movingWindow.get('content.windowId')
});
dispatch('CHANGE_ACTIVE_DESKTOP', {
desktopId
});
return;
}
if (cursor.x === null) {
return;
}
for (const [rowIndex, row] of this.dimensions.entries()) {
for (const [columnIndex, masWindow] of row.entries()) {
masWindow.cursor = 'none';
let deltaX = 0;
let deltaY = 0;
const oldRow = masWindow.component.get('row');
const oldColumn = masWindow.component.get('column');
if (cursor.section === 'top' || cursor.section === 'bottom') {
deltaY = rowIndex > cursor.y || (rowIndex === cursor.y && cursor.section === 'top') ? 1 : 0;
} else {
deltaX =
rowIndex === cursor.y && (columnIndex > cursor.x || (columnIndex === cursor.x && cursor.section === 'left'))
? 1
: 0;
}
const newRow = rowIndex + deltaY;
const newColumn = columnIndex + deltaX;
if (oldRow !== newRow || oldColumn !== newColumn) {
dispatch('MOVE_WINDOW', {
column: newColumn,
row: newRow,
windowId: masWindow.component.get('content.windowId')
});
}
}
}
const newColumn = cursor.x + (cursor.section === 'right' ? 1 : 0);
const newRow = cursor.y + (cursor.section === 'bottom' ? 1 : 0);
dispatch('MOVE_WINDOW', {
column: newColumn,
row: newRow,
windowId: this.movingWindow.get('content.windowId')
});
},
_layoutWindows(animate) {
const duration = animate ? 600 : 0;
const windowComponents = this.windowComponents;
const container = this._containerDimensions();
const expandedWindow = windowComponents.findBy('expanded', true);
if (expandedWindow) {
expandedWindow.move(
{
left: 0,
top: 0,
width: container.width,
height: container.height
},
duration
);
return;
}
const visibleWindows = windowComponents.filterBy('visible');
const rowNumbers = visibleWindows.mapBy('row').uniq().sort();
const rowHeight = Math.round(container.height / rowNumbers.length);
const dimensions = [];
rowNumbers.forEach((row, rowIndex) => {
const windowsInRow = visibleWindows.filterBy('row', row).sortBy('column');
const windowWidth = Math.round(container.width / windowsInRow.length);
dimensions.push([]);
windowsInRow.forEach((windowComponent, columnIndex) => {
dimensions[rowIndex].push({
left: columnIndex * windowWidth,
top: rowIndex * rowHeight,
width: windowWidth,
height: rowHeight,
component: windowComponent
});
});
});
this.dimensions = dimensions;
this._animate(duration);
},
_markWindow(masWindow, x, y, cursor) {
masWindow.cursor = 'none';
const rowCount = this.dimensions.length;
const columnCount = this.dimensions[y].length;
const section = cursor.section;
if ((cursor.y === y && section === 'top') || (y > 0 && cursor.y === y - 1 && section === 'bottom')) {
masWindow.cursor = 'top';
} else if (
(cursor.y === y && section === 'bottom') ||
(y < rowCount - 1 && cursor.y === y + 1 && section === 'top')
) {
masWindow.cursor = 'bottom';
} else if (
cursor.y === y &&
((section === 'left' && cursor.x === x) || (x > 0 && cursor.x === x - 1 && section === 'right'))
) {
masWindow.cursor = 'left';
} else if (
cursor.y === y &&
((section === 'right' && cursor.x === x) || (x < columnCount - 1 && cursor.x - 1 === x && section === 'left'))
) {
masWindow.cursor = 'right';
}
},
_drawCursor(cursor) {
if (cursor.x === null) {
$('#window-cursor').hide();
return;
}
$('#window-cursor').show();
const container = this._containerDimensions();
let cursorPos = {};
const cursorWindow = this.dimensions[cursor.y][cursor.x];
const section = cursor.section;
if (section === 'top' || section === 'bottom') {
let topPos = (section === 'top' ? cursorWindow.top : cursorWindow.top + cursorWindow.height) - CURSORWIDTH / 2;
if (cursor.y === 0 && section === 'top') {
topPos = cursorWindow.top;
} else if (cursor.y === this.dimensions.length - 1 && section === 'bottom') {
topPos = cursorWindow.top + cursorWindow.height - CURSORWIDTH;
}
cursorPos = {
left: 0,
width: container.width,
top: topPos,
height: CURSORWIDTH
};
} else {
let leftPos = (section === 'left' ? cursorWindow.left : cursorWindow.left + cursorWindow.width) - CURSORWIDTH / 2;
if (cursor.x === 0 && section === 'left') {
leftPos = cursorWindow.left;
} else if (cursor.x === this.dimensions[cursor.y].length - 1 && section === 'right') {
leftPos = cursorWindow.left + cursorWindow.width - CURSORWIDTH;
}
cursorPos = {
left: leftPos,
width: CURSORWIDTH,
top: this.dimensions[cursor.y][0].top,
height: this.dimensions[cursor.y][0].height
};
}
$('#window-cursor').css(cursorPos);
},
_animate(duration) {
this.dimensions.forEach((row, rowIndex) => {
row.forEach((windowDim, columnIndex) => {
const cursorSpace = this._calculateSpaceForCursor(columnIndex, rowIndex, windowDim.cursor);
const newDim = {
left: windowDim.cursor === 'left' ? windowDim.left + cursorSpace : windowDim.left,
top: windowDim.cursor === 'top' ? windowDim.top + cursorSpace : windowDim.top,
width:
windowDim.cursor === 'left' || windowDim.cursor === 'right'
? windowDim.width - cursorSpace
: windowDim.width,
height:
windowDim.cursor === 'top' || windowDim.cursor === 'bottom'
? windowDim.height - cursorSpace
: windowDim.height
};
const windowComponent = windowDim.component;
windowComponent.move(newDim, duration);
});
});
},
_calculateSpaceForCursor(x, y, cursor) {
let width = Math.round(CURSORWIDTH / 2);
const rows = this.dimensions.length;
const columns = this.dimensions[y].length;
if (
(y === 0 && cursor === 'top') ||
(y === rows - 1 && cursor === 'bottom') ||
(x === 0 && cursor === 'left') ||
(x === columns - 1 && cursor === 'right')
) {
width = CURSORWIDTH;
}
return width;
},
_containerDimensions() {
return {
x: this.$().offset().left,
width: this.$().width(),
height: this.$().height()
};
},
_calculateCursorPosition(event) {
const outOfBoundsCursor = { x: null, y: null, section: null };
const x = event.clientX;
const y = event.clientY;
let windowX = 0;
let windowY = 0;
let masWindow;
if (x < this._containerDimensions().x - 10) {
return outOfBoundsCursor;
}
this.dimensions.forEach((row, index) => {
if (row[0].top < y) {
windowY = index;
}
});
this.dimensions[windowY].forEach((column, index) => {
if (column.left < x) {
windowX = index;
masWindow = column;
}
});
if (!masWindow) {
return outOfBoundsCursor;
}
return {
x: windowX,
y: windowY,
section: this._whichSection(masWindow, x, y)
};
},
_whichSection(windowDim, x, y) {
// -----------------
// |\ t /|
// | \ / |
// | ----------- |
// |l | m | r|
// | ----------- |
// | / b \ |
// |/ \|
// -----------------
const BORDER = 50; // Defines the non-active area ('n' in the figure)
let topOrRight = true;
let bottomOrRight = false;
if (
windowDim.left + BORDER < x &&
windowDim.left + windowDim.width - BORDER > x &&
windowDim.top + BORDER < y &&
windowDim.top + windowDim.height - BORDER > y
) {
return 'middle'; // m
}
if (windowDim.height * (x - windowDim.left) < windowDim.width * (y - windowDim.top)) {
topOrRight = false;
}
if (windowDim.height * (windowDim.left + windowDim.width - x) < windowDim.width * (y - windowDim.top)) {
bottomOrRight = true;
}
if (topOrRight && !bottomOrRight) {
return 'top'; // t
}
if (topOrRight && bottomOrRight) {
return 'right'; // r
}
if (!topOrRight && bottomOrRight) {
return 'bottom'; // b
}
if (!topOrRight && !bottomOrRight) {
return 'left'; // l
}
return null;
}
});