Created
June 23, 2018 06:30
-
-
Save bradennapier/e982f7a0908c70a120f50c70eb1db8de to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { getDashboardWidgetSchema } from './utils/widgetSchema' | |
const getWidgetDefaultSchema = widgetName => { | |
const schema = getDashboardWidgetSchema(widgetName) | |
if ( ! schema ) { | |
throw new Error(`[Grid]: Widget Not Known: ${widgetName}`) | |
} | |
return schema | |
} | |
/* | |
A grid widget is an entry on the current grid. It is responsible for | |
assisting the grid in making the best decisions based on its schema. | |
*/ | |
export default class GridWidget { | |
constructor(widgetName, path, parent, root) { | |
console.log('Construct Leaf', path) | |
this.root = root | |
this.path = path | |
this.parent = parent | |
this.merge(widgetName) | |
this.root.addLeaf(widgetName, this) | |
} | |
constraints = () => { | |
const { constraints } = this.schema | |
return { | |
width: { | |
px: constraints.width, | |
pct: constraints.widthPercent, | |
min: constraints.minWidth || 0, | |
}, | |
height: { | |
px: constraints.height, | |
pct: constraints.heightPercent, | |
min: constraints.minHeight || 0, | |
}, | |
behavior: this.schema.behavior | |
} | |
} | |
behavior = () => this.schema.behavior | |
merge = widgetName => { | |
// get the schema for the new widget - if it is invalid | |
// this will throw an error which will stop the new widget | |
// from moving further | |
if ( this.name !== widgetName ) { | |
this.schema = getWidgetDefaultSchema(widgetName) | |
this.name = widgetName | |
} | |
} | |
remove = () => { | |
this.isRemoved = true | |
this.root.removeLeaf(this.name, this) | |
} | |
update = values => { | |
for ( let value in values ) { | |
this[value] = values[value] | |
} | |
} | |
has = widget => widget === this | |
compile = () => this.name | |
build = () => { | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { getDashboardWidgetSchema } from './utils/widgetSchema'; | |
import GridWidget from './grid-widget'; | |
const getDefaultLevel = (first, second) => ({ | |
first, | |
second, | |
splitPercentage: 80, | |
direction: 'row', | |
}); | |
const getDefaultGrid = () => ({ | |
first: 'projectDashboard', | |
second: 'projectList', | |
splitPercentage: 80, | |
direction: 'row', | |
}); | |
const SeparatorPx = 2; | |
const NavbarHeight = 60; | |
const isGridClass = value => value instanceof Grid || value instanceof GridWidget; | |
class Grid { | |
constructor(grid, path, parent, root) { | |
// console.log('Create: ', grid) | |
this.parent = parent; | |
this.root = root; | |
this.path = path; | |
this.side = path.split('.').pop(); | |
if (this.side !== 'root') { | |
this.sibling = this.side === 'first' ? 'second' : 'first'; | |
} | |
this.grid = {}; | |
this.root.addBranch(path, this); | |
this.replace(grid); | |
} | |
remove = () => { | |
this.isRemoved = true; | |
this.root.removeBranch(this.path); | |
if (this.grid.first) { | |
this.grid.first.remove(); | |
} | |
if (this.grid.second) { | |
this.grid.second.remove(); | |
} | |
return this; | |
}; | |
replace = grid => { | |
delete this.grid.splitPercentage; | |
delete this.grid.direction; | |
this.merge(grid, { replace: true }); | |
return this; | |
}; | |
merge = (grid, params = { replace: false }) => { | |
if (params.replace) { | |
this.grid.splitPercentage = grid.splitPercentage; | |
this.grid.direction = grid.direction; | |
} else { | |
if (grid.splitPercentage) { | |
this.grid.splitPercentage = grid.splitPercentage; | |
} | |
if (grid.direction) { | |
this.grid.direction = grid.direction; | |
} | |
} | |
for (const key of ['first', 'second']) { | |
const value = grid[key]; | |
if (value === undefined) { | |
if (params.replace && this.grid[key]) { | |
if (isGridClass(this.grid[key])) { | |
this.grid[key].remove(); | |
} | |
delete this.grid[key]; | |
} | |
continue; | |
} | |
const path = `${this.path}.${key}`; | |
if (isGridClass(this.grid[key])) { | |
this.grid[key].remove(); | |
} | |
if (typeof value === 'string') { | |
this.grid[key] = new GridWidget(value, path, this, this.root); | |
} else if (typeof value === 'object') { | |
this.grid[key] = new Grid(value, path, this, this.root); | |
} | |
} | |
return this; | |
}; | |
removeChild = child => { | |
const sibling = child === 'first' ? 'second' : 'first'; | |
const childToRemove = this.grid[child]; | |
const childToRetain = this.grid[sibling]; | |
const retainedChild = childToRetain.compile(); | |
const side = this.side; | |
// console.log('Retaining : ', this.path, retainedChild) | |
if (this.parent.root === true) { | |
// removing a root-level child -- | |
if (childToRetain instanceof Grid) { | |
this.merge({ | |
...retainedChild, | |
splitPercentage: 'auto', | |
}); | |
} else { | |
this.merge({ | |
splitPercentage: 100, | |
[child]: undefined, | |
}); | |
} | |
} else { | |
this.parent.merge({ | |
[side]: retainedChild, | |
splitPercentage: 'auto', | |
}); | |
} | |
}; | |
replaceChild = (child, widgetName, merge = {}) => { | |
const update = {}; | |
if (merge.splitPercentage) { | |
if (child === 'first') { | |
update.splitPercentage = merge.splitPercentage; | |
} else { | |
update.splitPercentage = 100 - merge.splitPercentage; | |
} | |
} | |
if (merge.direction) { | |
update.direction = merge.direction; | |
} | |
this.merge({ | |
...update, | |
[child]: widgetName, | |
}); | |
}; | |
// Flip the children around | |
switchChildren = () => { | |
this.merge({ | |
first: this.grid.second.compile(), | |
second: this.grid.first.compile(), | |
}); | |
}; | |
switchDirection = direction => { | |
console.log('Direction: ', direction, this.grid.direction); | |
const update = {}; | |
if (direction) { | |
console.log('Set Direction: ', direction); | |
update.direction = direction; | |
} else { | |
console.log('Direction: ', direction, ' does not exist'); | |
update.direction = this.grid.direction === 'row' ? 'column' : 'row'; | |
} | |
this.merge(update); | |
console.log(this.grid.direction); | |
}; | |
removeWidgetFromPath = (widgetName, path) => {}; | |
constraints = () => { | |
const first = this.grid.first.constraints(); | |
const second = this.grid.second.constraints(); | |
return { | |
first, | |
second, | |
width: { | |
px: first.width.px + second.width.px, | |
pct: first.width.pct + second.width.pct, | |
min: first.width.min + second.width.min, | |
}, | |
height: { | |
px: first.height.px + second.height.px, | |
pct: first.height.pct + second.height.pct, | |
min: first.height.min + second.height.min, | |
}, | |
behavior: { | |
grow: first.behavior.grow || second.behavior.grow, | |
}, | |
}; | |
}; | |
calculateSplitPercentage = () => { | |
if (this.grid.splitPercentage === 'auto') { | |
const constraints = this.constraints(); | |
const dimensions = this.getSelfDimensions(); | |
const rootDimensions = this.root.getDimensions(); | |
const parentDimensions = this.parent.getDimensions(); | |
if (this.grid.direction === 'auto') { | |
this.calculateDirection(); | |
} | |
if (this.grid.direction === 'row') { | |
// if ( constraints.width.px <= dimensions.width ) { | |
// } else if ( constraints.width.min <= dimensions.width ) { | |
// // When we have min width values and need to use them, we will | |
// // determine how much each side is capable of shrinking to try | |
// // to make the content look as good as possible | |
// const firstMin = constraints.first.width.min | |
// const secondMin = constraints.second.width.min | |
// const extraWidth = constraints.width.px - dimensions.width | |
// } | |
// When we have enough px to fit our widgets then we will add them | |
// in at their width levels. We will then grow the width using the | |
// scale given by each sides percent values. | |
const firstPx = constraints.first.width.px; | |
const firstPct = firstPx / dimensions.width * 100; | |
const secondPx = constraints.second.width.px; | |
const secondPct = secondPx / dimensions.width * 100; | |
if (firstPct + secondPct < 100) { | |
const remainingPct = 100 - (firstPct + secondPct); | |
/* Using the min values, we will split the difference between the two | |
sides. We will grow the side which has a lesser min value a bit | |
more than the other -- although this is not currently done to ratio. | |
*/ | |
let remainingModifier; | |
if (constraints.first.behavior.grow && !constraints.second.behavior.grow) { | |
// The first should grow and the second should not | |
remainingModifier = remainingPct; | |
} else if (!constraints.first.behavior.grow && constraints.second.behavior.grow) { | |
// The second should grow and the first should not | |
remainingModifier = 0; | |
} else if (constraints.first.width.min < constraints.second.width.min) { | |
remainingModifier = remainingPct / 2.5; | |
} else if (constraints.first.width.min > constraints.second.width.min) { | |
remainingModifier = remainingPct / 1.5; | |
} else { | |
remainingModifier = remainingPct / 2; | |
} | |
this.grid.splitPercentage = firstPct + remainingModifier; | |
} else if (firstPct + secondPct > 100) { | |
const extraPct = firstPct + secondPct - 100; | |
let remainingModifier; | |
/* Using the min values, we will split the difference between the two | |
sides. We will shrink the side which has a lesser min value a bit | |
more than the other -- although this is not currently done to ratio. | |
*/ | |
if (constraints.first.width.min < constraints.second.width.min) { | |
remainingModifier = 1.5; | |
} else if (constraints.first.width.min > constraints.second.width.min) { | |
remainingModifier = 2.5; | |
} else { | |
remainingModifier = 2; | |
} | |
this.grid.splitPercentage = firstPct - extraPct / remainingModifier; | |
} else { | |
this.grid.splitPercentage = firstPct; | |
} | |
} else { | |
this.grid.splitPercentage = constraints.first.height.pct; | |
} | |
if (this.grid.splitPercentage < 20) { | |
this.grid.splitPercentage = 20; | |
} else if (this.grid.splitPercentage > 80) { | |
this.grid.splitPercentage = 80; | |
} | |
} else { | |
return this.grid.splitPercentage; | |
} | |
}; | |
calculateDirection = () => { | |
if (this.grid.direction === 'auto') { | |
/* When direction is set to auto, we will determine the best | |
direction to use based upon the current constraints that we | |
have and the dimensions available to us. | |
*/ | |
// console.log(this.path, ' calculating direction!') | |
const dimensions = this.getSelfDimensions(); | |
const constraints = this.constraints(); | |
if (constraints.width.px <= dimensions.width) { | |
this.grid.direction = 'row'; | |
} else if (constraints.width.min <= dimensions.width) { | |
this.grid.direction = 'row'; | |
} else if (constraints.height.px <= dimensions.height) { | |
this.grid.direction = 'column'; | |
} else if (constraints.height.min <= dimensions.height) { | |
this.grid.direction = 'column'; | |
} else { | |
// If we can not fit within the px constraints, we need to determine | |
// the best possible fit. | |
// the width needed to match the constraints | |
const widthOffset = constraints.width.px - dimensions.width; | |
// the height needed to match the constraints | |
const heightOffset = constraints.height.px - dimensions.height; | |
if (widthOffset <= heightOffset) { | |
this.grid.direction = 'row'; | |
} else { | |
this.grid.direction = 'column'; | |
} | |
} | |
// console.log('Calculated Direction: ', this.grid.direction) | |
} | |
return this.grid.direction; | |
}; | |
/* gets our dimensions from the parent container by calling getDimensions | |
and returning either our side of the parent or, if we are the root's grid, | |
returning the window dimensions. | |
*/ | |
getSelfDimensions = () => | |
this.side === 'root' | |
? { ...this.parent.getDimensions() } | |
: { ...this.parent.getDimensions()[this.side] }; | |
/* calculate the dimensions of each member of the grid based upon the | |
direction and size of the parent and the splitPercentage of this | |
grid. This works by getting the dimensions of the entire area then | |
use the splitPercentage to attempt to calculate each sides dimensions. | |
IMPORTANT* This is an estimated value -- it is likely to be off by a | |
few px based upon styling such as padding. | |
*/ | |
getDimensions = () => { | |
const dimensions = this.getSelfDimensions(); | |
// console.log(` | |
// -- Get Childs Dimensions ${this.path} | ${this.side} -- | |
// `, dimensions, this.grid.direction, this.grid.splitPercentage) | |
if (this.grid.direction === 'auto') { | |
this.calculateDirection(); | |
} | |
if (this.grid.splitPercentage === 'auto') { | |
this.calculateSplitPercentage(); | |
} | |
if (this.grid.direction === 'row') { | |
dimensions.first = { | |
width: dimensions.width * (this.grid.splitPercentage / 100) - SeparatorPx, | |
height: dimensions.height, | |
}; | |
dimensions.second = { | |
width: dimensions.width * ((100 - this.grid.splitPercentage) / 100) - SeparatorPx, | |
height: dimensions.height, | |
}; | |
} else if (this.grid.direction === 'column') { | |
dimensions.first = { | |
width: dimensions.width, | |
height: dimensions.height * (this.grid.splitPercentage / 100) - SeparatorPx, | |
}; | |
dimensions.second = { | |
width: dimensions.width, | |
height: dimensions.height * ((100 - this.grid.splitPercentage) / 100) - SeparatorPx, | |
}; | |
} | |
return dimensions; | |
}; | |
/* When a child wants to have a certain split percentage of its parent, it will | |
call this which will determine if the given split percentage is possible. It | |
will return a new split percentage that is either the value requested or the | |
maximum that it will allow. If the caller is happy with the split, it can then | |
call setParentSplitPercentage with that value. | |
This allows us to determine if we will be allowed to grow to meet our widgets | |
constraints sufficiently. It may otherwise decide to switch it's direction if | |
that ends up being a better fit. | |
If no changes will be allowed, undefined will be returned. | |
*/ | |
requestParentSplitPercentage = (split, direction) => { | |
if (this.parent.root === true) { | |
// we are already the root level so we can not expand further than the | |
// root dimensions. | |
} else if (this.parent.grid.direction !== direction) { | |
// If our parent is not the same direction then we can not | |
// allow it to change as it will not be changing in the correct | |
// direction. | |
} else { | |
const parentConstraints = this.parent.constraints(); | |
const siblingConstraints = parentConstraints[this.sibling]; | |
const requestedSplit = this.side === 'first' ? split : 100 - split; | |
} | |
}; | |
setParentSplitPercentage = split => { | |
if (this.parent.root === true) { | |
} else { | |
const requestedSplit = this.side === 'first' ? split : 100 - split; | |
this.parent.merge({ | |
splitPercentage: requestedSplit, | |
}); | |
} | |
}; | |
compile = () => { | |
const splitPercentage = this.calculateSplitPercentage(); | |
const direction = this.calculateDirection(); | |
const first = this.grid.first.compile(); | |
const second = this.grid.second.compile(); | |
return { | |
first, | |
second, | |
direction: direction || this.grid.direction, | |
splitPercentage: splitPercentage || this.grid.splitPercentage, | |
}; | |
}; | |
} | |
// Grid is the root level and manages the API to the grid. | |
export default class GridRoot { | |
constructor(grid) { | |
this.root = true; | |
this.leaves = new Map(); | |
this.branches = new Map(); | |
this.grid = new Grid(grid, 'root', this, this); | |
return this; | |
} | |
addLeaf = (name, context) => { | |
// console.log('Add Leaf: ', name, context) | |
const leaf = this.leaves.get(name) || new Set(); | |
console.log(leaf); | |
leaf.add(context); | |
this.leaves.set(name, leaf); | |
}; | |
removeLeaf = (name, context) => { | |
if (this.leaves.has(name)) { | |
// console.log('Remove Leaf: ', name) | |
const leaf = this.leaves.get(name); | |
if (leaf) { | |
leaf.delete(context); | |
this.leaves.set(name, leaf); | |
} | |
} | |
}; | |
addBranch = (path, context) => { | |
this.removeBranch(path); | |
// console.log('Add Branch: ', path, context) | |
this.branches.set(path, context); | |
}; | |
removeBranch = path => { | |
if (this.branches.has(path)) { | |
// console.log('Remove Branch: ', path) | |
const prev_branch = this.branches.get(path); | |
this.branches.delete(path); | |
if (prev_branch && !prev_branch.isRemoved) { | |
prev_branch.remove(); | |
} | |
} | |
}; | |
pathToWidget = widgetName => { | |
// console.log('Getting Path To Widget: ', widgetName) | |
const leaves = this.leaves.get(widgetName); | |
if (!leaves) { | |
return; | |
} | |
const paths = []; | |
for (const leaf of leaves) { | |
paths.push(leaf.path); | |
} | |
// console.log(paths) | |
return paths.length > 0 && paths; | |
}; | |
compile = () => this.grid && this.grid.compile(); | |
replace = (...args) => this.grid.replace(...args); | |
addWidgetToPath = (widgetName, paths, direction = 'auto', splitPercentage = 'auto') => { | |
paths = this.formatPaths(paths); | |
if (!paths) { | |
return; | |
} | |
for (const path of paths) { | |
const route = path.split('.'); | |
const addToKey = route.pop(); | |
const branch = this.branches.get(route.join('.')); | |
branch.merge({ | |
[addToKey]: { | |
first: branch.grid[addToKey].compile(), | |
second: widgetName, | |
direction, | |
splitPercentage, | |
}, | |
splitPercentage: 'auto', | |
}); | |
// console.log('Branch of Widget: ', path, branch, this.branches) | |
} | |
return this; | |
}; | |
removeWidgetFromPath = (widgetName, paths) => { | |
paths = this.formatPaths(paths); | |
if (!paths) { | |
return; | |
} | |
for (const path of paths) { | |
const route = path.split('.'); | |
const removeFromKey = route.pop(); | |
const branch = this.branches.get(route.join('.')); | |
// console.log(` | |
// Route: ${route.join('.')} | |
// Remove From Key: ${removeFromKey} | |
// `, branch) | |
if (!branch) { | |
console.error('Unknown Branch for Route; ', route); | |
return; | |
} | |
branch.removeChild(removeFromKey); | |
} | |
return this; | |
}; | |
replaceWidgetAtPathWith = (widgetName, paths, merge) => { | |
paths = this.formatPaths(paths); | |
if (!paths) { | |
return; | |
} | |
for (const path of paths) { | |
const { branch, key } = this.getBranch(path); | |
if (!branch || !key) { | |
console.error('Branch Not Found at Path: ', path, key); | |
return; | |
} | |
branch.replaceChild(key, widgetName, merge); | |
} | |
}; | |
switchChildrenAtPath = paths => { | |
paths = this.formatPaths(paths); | |
if (!paths) { | |
return; | |
} | |
for (const path of paths) { | |
const { branch } = this.getBranch(path); | |
if (!branch) { | |
console.error('Failed to Get Branch, Not Found'); | |
return; | |
} | |
branch.switchChildren(); | |
} | |
}; | |
switchDirectionAtPath = (paths, direction) => { | |
paths = this.formatPaths(paths); | |
if (!paths) { | |
return; | |
} | |
for (const path of paths) { | |
const { branch } = this.getBranch(path); | |
if (!branch) { | |
console.error('Failed to Get Branch, Not Found'); | |
return; | |
} | |
branch.switchDirection(direction); | |
} | |
}; | |
/* Takes a path and gets the closest branch (either the path itself or | |
the parent branch of the path). | |
*/ | |
getBranch = path => { | |
if (this.branches.has(path)) { | |
return { branch: this.branches.get(path) }; | |
} | |
const route = path.split('.'); | |
const key = route.pop(); | |
return { | |
branch: this.branches.get(route.join('.')), | |
key, | |
}; | |
}; | |
formatPaths = paths => { | |
if (!paths) { | |
return; | |
} | |
if (!Array.isArray(paths)) { | |
paths = [paths]; | |
} | |
return paths; | |
}; | |
getDimensions = () => ({ | |
width: window.innerWidth, | |
height: window.innerHeight - NavbarHeight, | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment