src/utils/windows.js
/*
* OS.js - JavaScript Cloud/Web Desktop Platform
*
* Copyright (c) Anders Evenrud <andersevenrud@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Anders Evenrud <andersevenrud@gmail.com>
* @license Simplified BSD License
*/
import * as mediaQuery from 'css-mediaquery';
import defaultIcon from '../styles/logo-blue-32x32.png';
import logger from '../logger';
const CASCADE_DISTANCE = 10;
const MINIMUM_WIDTH = 100;
const MINIMUM_HEIGHT = 100;
const ONTOP_ZINDEX = 8388635;
const BOTTOM_ZINDEX = 10;
/*
* Creates window attributes from an object
*/
export const createAttributes = attrs => ({
classNames: [],
modal: false,
ontop: false,
gravity: false,
moveable: true,
resizable: true,
focusable: true,
maximizable: true,
minimizable: true,
sessionable: true,
closeable: true,
header: true,
controls: true,
visibility: 'global',
shadowDOM: false,
clamp: true,
droppable: true,
mediaQueries: {
small: 'screen and (max-width: 640px)',
medium: 'screen and (min-width: 640px) and (max-width: 1024px)',
big: 'screen and (min-width: 1024px)'
},
minDimension: {
width: MINIMUM_WIDTH,
height: MINIMUM_HEIGHT
},
maxDimension: {
width: -1,
height: -1
},
...attrs
});
/*
* Creates window state from an object
*/
export const createState = (state, options, attrs) => ({
title: options.title || options.id,
icon: options.icon || defaultIcon,
media: null,
moving: false,
resizing: false,
loading: false,
focused: false,
maximized: false,
minimized: false,
zIndex: 1,
styles: {},
position: {
left: null,
top: null,
...options.position
},
dimension: {
width: Math.max(attrs.minDimension.width, MINIMUM_WIDTH),
height: Math.max(attrs.minDimension.height, MINIMUM_HEIGHT),
...options.dimension
},
...state
});
/**
* Creates data attributes for window DOM
* @param {string} id
* @param {WindowState} state
* @param {WindowAttributes} attributes
* @return {object}
*/
export const createDOMAttributes = (id, state, attributes) => ({
id: id,
media: state.media,
moving: state.moving,
resizing: state.resizing,
loading: state.loading,
focused: state.focused,
maximized: state.maximized,
minimized: state.minimized,
modal: attributes.modal,
ontop: attributes.ontop,
resizable: attributes.resizable,
moveable: attributes.moveable,
maximizable: attributes.maximizable,
minimizable: attributes.minimizable
});
/**
* Creates styles for window DOM
* @param {WindowState} state
* @param {WindowAttributes} attributes
* @return {object}
*/
export const createDOMStyles = (
{
zIndex,
styles,
position: {
top,
left
},
dimension: {
width,
height
}
},
{
ontop
}
) => ({
top: String(top) + 'px',
left: String(left) + 'px',
height: String(height) + 'px',
width: String(width) + 'px',
zIndex: BOTTOM_ZINDEX + (ontop ? ONTOP_ZINDEX : 0) + zIndex,
...styles
});
/*
* Clamps position to viewport
*/
export const clampPosition = (rect, {dimension, position}) => {
const maxLeft = rect.width - dimension.width;
const maxTop = rect.height - dimension.height + rect.top;
// TODO: Make these an argument ?!
const minLeft = 0;
const minTop = 0;
return {
left: Math.max(minLeft, Math.min(maxLeft, position.left)),
top: Math.max(minTop, Math.max(rect.top, Math.min(maxTop, position.top)))
};
};
/*
* Window rendering callback function
*/
export const renderCallback = (win, callback) => {
if (typeof callback === 'function') {
if (win.attributes.shadowDOM) {
try {
const mode = typeof win.attributes.shadowDOM === 'string'
? win.attributes.shadowDOM
: 'open';
const shadow = win.$content.attachShadow({mode});
callback(shadow, win);
return;
} catch (e) {
logger.warn('Shadow DOM not supported?', e);
}
}
callback(win.$content, win);
}
};
/*
* Gets new position based on "gravity"
*/
export const positionFromGravity = (win, rect, gravity) => {
let {left, top} = win.state.position;
if (gravity === 'center') {
left = (rect.width / 2) - (win.state.dimension.width / 2);
top = (rect.height / 2) - (win.state.dimension.height / 2);
} else if (gravity) {
let hasVertical = gravity.match(/top|bottom/);
let hasHorizontal = gravity.match(/left|rigth/);
if (gravity.match(/top/)) {
top = rect.top;
} else if (gravity.match(/bottom/)) {
top = rect.height - (win.state.dimension.height) + rect.top;
}
if (gravity.match(/left/)) {
left = rect.left;
} else if (gravity.match(/right/)) {
left = rect.width - (win.state.dimension.width);
}
if (!hasVertical && gravity.match(/center/)) {
top = (rect.height / 2) - (win.state.dimension.height / 2);
} else if (!hasHorizontal && gravity.match(/center/)) {
left = (rect.width / 2) - (win.state.dimension.width / 2);
}
}
return {left, top};
};
/*
* Gets new dimension based on container
*/
export const dimensionFromElement = (win, rect, container) => {
const innerBox = (container.parentNode.classList.contains('osjs-gui')
? container.parentNode
: container).getBoundingClientRect();
const outerBox = win.$content.getBoundingClientRect();
const diffY = Math.ceil(outerBox.height - innerBox.height);
const diffX = Math.ceil(outerBox.width - innerBox.width);
const topHeight = win.$header.offsetHeight;
const {left, top} = win.state.position;
const min = win.attributes.minDimension;
const max = win.attributes.maxDimension;
let width = Math.max(container.offsetWidth + diffX, min.width);
let height = Math.max(container.offsetHeight + diffY + topHeight, min.height);
if (max.width > 0) {
width = Math.min(width, max.width);
}
if (max.height > 0) {
height = Math.min(height, max.height);
}
width = Math.max(width, container.offsetWidth);
height = Math.max(height, container.offsetHeight);
if (rect) {
width = Math.min(width, rect.width - left);
height = Math.min(height, rect.height - top);
}
return {width, height};
};
/*
* Transforms vector values (ex float to integer)
*/
export const transformVectors = (rect, {width, height}, {top, left}) => {
const transform = (val, attr) => {
if (!isNaN(val)) {
return val > 1 && Number.isInteger(val)
? val
: Math.round(rect[attr] * parseFloat(val));
}
return val;
};
return {
dimension: {
width: transform(width, 'width'),
height: transform(height, 'height')
},
position: {
top: transform(top, 'height'),
left: transform(left, 'width')
}
};
};
/*
* Creates a clamper for resize/move
*/
const clamper = win => {
const {maxDimension, minDimension} = win.attributes;
const {position, dimension} = win.state;
const maxPosition = {
left: position.left + dimension.width - minDimension.width,
top: position.top + dimension.height - minDimension.height
};
const clamp = (min, max, current) => {
const value = min === -1 ? current : Math.max(min, current);
return max === -1 ? value : Math.min(max, value);
};
return (width, height, top, left) => ({
width: clamp(minDimension.width, maxDimension.width, width),
height: clamp(minDimension.height, maxDimension.height, height),
top: clamp(-1, maxPosition.top, top),
left: clamp(-1, maxPosition.left, left)
});
};
/*
* Creates a resize handler
*/
export const resizer = (win, handle) => {
const clamp = clamper(win);
const {position, dimension} = win.state;
const directions = handle.getAttribute('data-direction').split('');
const going = dir => directions.indexOf(dir) !== -1;
const xDir = going('e') ? 1 : (going('w') ? -1 : 0);
const yDir = going('s') ? 1 : (going('n') ? -1 : 0);
return (diffX, diffY) => {
const width = dimension.width + (diffX * xDir);
const height = dimension.height + (diffY * yDir);
const top = yDir === -1 ? position.top + diffY : position.top;
const left = xDir === -1 ? position.left + diffX : position.left;
return clamp(width, height, top, left);
};
};
/*
* Creates a movement handler
*/
export const mover = (win, rect) => {
const {position} = win.state;
return (diffX, diffY) => {
const top = Math.max(position.top + diffY, rect.top);
const left = position.left + diffX;
return {top, left};
};
};
/*
* Calculates a new initial position for window
*/
export const getCascadePosition = (win, rect, pos) => {
const startX = CASCADE_DISTANCE + rect.left;
const startY = CASCADE_DISTANCE + rect.top;
const distance = CASCADE_DISTANCE;
const wrap = CASCADE_DISTANCE * 2;
const newX = startX + ((win.wid % wrap) * distance);
const newY = startY + ((win.wid % wrap) * distance);
const position = (key, value) => typeof pos[key] === 'number' && Number.isInteger(pos[key])
? Math.max(rect[key], pos[key])
: value;
return {top: position('top', newY), left: position('left', newX)};
};
const getScreenOrientation = screen => screen && screen.orientation
? screen.orientation.type
: window.matchMedia('(orientation: portrait)') ? 'portrait' : 'landscape';
/*
* Gets a media query name from a map
*/
export const getMediaQueryName = (win) => Object.keys(win.attributes.mediaQueries)
.filter(name => mediaQuery.match(win.attributes.mediaQueries[name], {
type: 'screen',
orientation: getScreenOrientation(window.screen),
width: win.$element.offsetWidth || win.state.dimension.width,
height: win.$element.offsetHeight || win.state.dimension.height
}))
.pop();
/**
* Loads [certain] window options from configuration
*/
export const loadOptionsFromConfig = (config, appName, windowId) => {
const matchStringOrRegex = (str, matcher) => matcher instanceof RegExp
? !!str.match(matcher)
: str === matcher;
const found = config.find(({application, window}) => {
if (!application && !window) {
return false;
} else if (application && !matchStringOrRegex(appName, application)) {
return false;
} else if (window && !matchStringOrRegex(windowId || '', window)) {
return false;
}
return true;
});
const foundOptions = found && found.options ? found.options : {};
const allowedProperties = ['position', 'dimension', 'attributes'];
return allowedProperties.reduce((obj, key) => (foundOptions[key]
? {...obj, [key]: foundOptions[key]}
: obj),
{});
};