From 4177cafa88b464e8682126534bc8cb1644f9fbce Mon Sep 17 00:00:00 2001 From: Patrick Sun Date: Thu, 3 Mar 2016 23:09:29 +0800 Subject: [PATCH] Pane resizing and nested/vertical pane groups (fixes #91 and #92) In resize.js, the buildPaneTree() method loops through the DOM recursively and gets all the pane and pane-groups. During this process, splitter elements are created and appended between panes and the resizer() method is attached to each splitter onmousedown. The resizer() method takes the initial cursor position and sets the flex-basis of its neighboring panes based on the distance moved. Additionally, pane-groups can be nested in one another (`
`) and a new class, `.pane-group-vertical`, is used to stack panes vertically. None of these changes will affect anything that has already been built. --- dist/template-app/js/resize.js | 246 +++++++++++++++++++++++++++++ dist/template-app/resize-demo.html | 193 ++++++++++++++++++++++ sass/grid.scss | 23 ++- sass/photon.scss | 1 + sass/splitters.scss | 37 +++++ 5 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 dist/template-app/js/resize.js create mode 100644 dist/template-app/resize-demo.html create mode 100644 sass/splitters.scss diff --git a/dist/template-app/js/resize.js b/dist/template-app/js/resize.js new file mode 100644 index 0000000..180290a --- /dev/null +++ b/dist/template-app/js/resize.js @@ -0,0 +1,246 @@ +// Global drag-related variables +var CURSOR_IN_DRAG_ZONE = false, + CAN_DRAG = false, + IS_DRAGGING = false; + +// Get the root .pane-group element +var rootPaneGroup = Array.from(document.getElementsByClassName('pane-group'))[0]; + +// Build pane tree +buildPaneTree(rootPaneGroup); + +// Rebuild tree on resize +window.onresize = function() { buildPaneTree(rootPaneGroup); } + +// Build pane tree recursively +function buildPaneTree(el) { + // Base case + if (isPane(el) && !isPaneGroup(el)) { + return ({ node: el }); + } else { + if (isPaneGroup(el)) { + var splitters; + + // Only allow .pane elements to pass through + var panes = objToArr(el.childNodes).filter(function(child) { + if (objToArr(child.classList).includes('pane')) return true; + else return false; + }); + + // Append splitters to DOM then split panes equally + appendSplitters(el, panes, isVerticalPaneGroup(el), function() { + var elementDimension, + splitterClass; + + // Set variables based on pane-group direction + if (isVerticalPaneGroup(el)) { + elementDimension = getExistingHeight(el); + splitterClass = 'splitter-vertical'; + } else { + elementDimension = getExistingWidth(el); + splitterClass = 'splitter'; + } + + // Set the flex-basis for each pane + panes.forEach(function(pane, i) { + if (pane.style.flexBasis === '') { + setFlexBasis(pane, (elementDimension / panes.length)); + } + }); + + // Push the splitters to an array + splitters = objToArr(el.childNodes).filter(function(child) { + if (objToArr(child.classList).includes(splitterClass)) return true; + else return false; + }); + }); + + splitters.forEach(function(s, i) { + s.onmousedown = resizer.bind(null, i, panes, isVerticalPaneGroup(el)); + }) + + return { + child_panes: panes.map(function(child) { + return buildPaneTree(child) + }), + node: el, + splitters: splitters, + vertical: isVerticalPaneGroup(el) + } + } + } +} + +function appendSplitters(el, panes, isVertical, setSplitters) { + // Appends a .splitter before every pane except for first child in the group + panes.forEach(function(pane, i) { + if (isVertical) { + if (i !== 0 && !objToArr(pane.previousSibling.classList).includes('splitter-vertical')) { + var splitter = document.createElement('div'), + zone = document.createElement('div'); + splitter.classList.add('splitter-vertical'); + zone.classList.add('splitter-zone-vertical'); + el.insertBefore(splitter, pane); + splitter.appendChild(zone); + } + } else { + if (i !== 0 && !objToArr(pane.previousSibling.classList).includes('splitter')) { + var splitter = document.createElement('div'), + zone = document.createElement('div'); + splitter.classList.add('splitter'); + zone.classList.add('splitter-zone'); + el.insertBefore(splitter, pane); + splitter.appendChild(zone); + } + } + }); + // Set each .pane's flex-basis property after splitters have + // been appended to the DOM + setSplitters(); +} + +function resizer(i, panes, vertical, e) { + CAN_DRAG = true; + + function resizePanes(prev, next, prevFlex, nextFlex) { + setFlexBasis(prev, prevFlex); + setFlexBasis(next, nextFlex); + } + + // Vertical .pane-group resizing + if (vertical) { + var initialCursorY = e.clientY, + topPane = panes[i], + bottomPane = panes[i + 1], + topPaneHeight = getExistingHeight(topPane), + bottomPaneHeight = getExistingHeight(bottomPane), + topPaneMaxHeight = getExistingMaxheight(topPane), + bottomPaneMaxHeight = getExistingMaxheight(bottomPane), + topPaneHasMaxHeight = !isNaN(topPaneMaxHeight), + bottomPaneHasMaxHeight = !isNaN(bottomPaneMaxHeight); + + document.onmousemove = function (e) { + if (CAN_DRAG) { + IS_DRAGGING = true; + var distanceTraveledY = initialCursorY - e.clientY, + newTopPaneHeight = topPaneHeight - distanceTraveledY, + newBottomPaneHeight = bottomPaneHeight + distanceTraveledY; + + // Allow resize only if new height < max height + if ((topPaneHasMaxHeight && !bottomPaneHasMaxHeight) && + (newTopPaneHeight <= topPaneMaxHeight)) { + resizePanes(topPane, bottomPane, newTopPaneHeight, newBottomPaneHeight); + } + else if ((!topPaneHasMaxHeight && bottomPaneHasMaxHeight) && + (newBottomPaneHeight <= bottomPaneMaxHeight)) { + resizePanes(topPane, bottomPane, newTopPaneHeight, newBottomPaneHeight); + } + else if ((topPaneHasMaxHeight && bottomPaneHasMaxHeight) && + (newTopPaneHeight <= topPaneMaxHeight) && + (newBottomPaneHeight <= bottomPaneMaxHeight)) { + resizePanes(topPane, bottomPane, newTopPaneHeight, newBottomPaneHeight); + } + else if (!topPaneHasMaxHeight && !bottomPaneHasMaxHeight) { + resizePanes(topPane, bottomPane, newTopPaneHeight, newBottomPaneHeight); + } + } + document.onmouseup = function (e) { + IS_DRAGGING = false; + CAN_DRAG = false; + } + } + } + // Horizontal .pane-group resizing + else { + var initialCursorX = e.clientX, + leftPane = panes[i], + rightPane = panes[i + 1], + leftPaneWidth = getExistingWidth(leftPane), + rightPaneWidth = getExistingWidth(rightPane), + leftPaneMaxWidth = getExistingMaxWidth(leftPane), + rightPaneMaxWidth = getExistingMaxWidth(rightPane), + leftPaneHasMaxWidth = !isNaN(leftPaneMaxWidth), + rightPaneHasMaxWidth = !isNaN(rightPaneMaxWidth); + + document.onmousemove = function (e) { + if (CAN_DRAG) { + IS_DRAGGING = true; + var distanceTraveledX = initialCursorX - e.clientX, + newLeftPaneWidth = leftPaneWidth - distanceTraveledX, + newRightPaneWidth = rightPaneWidth + distanceTraveledX; + + // Allow resize only if new width < max width + if ((leftPaneHasMaxWidth && !rightPaneHasMaxWidth) && + (newLeftPaneWidth <= leftPaneMaxWidth)) { + resizePanes(leftPane, rightPane, newLeftPaneWidth, newRightPaneWidth); + } + else if ((!leftPaneHasMaxWidth && rightPaneHasMaxWidth) && + (newRightPaneWidth <= rightPaneMaxWidth)) { + resizePanes(leftPane, rightPane, newLeftPaneWidth, newRightPaneWidth); + } + else if ((leftPaneHasMaxWidth && rightPaneHasMaxWidth) && + (newLeftPaneWidth <= leftPaneMaxWidth) && + (newRightPaneWidth <= rightPaneMaxWidth)) { + resizePanes(leftPane, rightPane, newLeftPaneWidth, newRightPaneWidth); + } + else if (!leftPaneHasMaxWidth && !rightPaneHasMaxWidth) { + resizePanes(leftPane, rightPane, newLeftPaneWidth, newRightPaneWidth); + } + } + document.onmouseup = function (e) { + IS_DRAGGING = false; + CAN_DRAG = false; + } + } + } +} + +// Utility Functions +// App-specific utilities +function isPane(el) { + return hasClass(el, 'pane'); +} +function isPaneGroup(el) { + return hasClass(el, 'pane-group'); +} +function isVerticalPaneGroup(el) { + return hasClass(el, 'pane-group-vertical'); +} +// General utilities +function objToArr(obj) { + if (Array.isArray(obj)) { + return obj; + } else { + var arr = []; + for (var key in obj) { + arr.push(obj[key]); + } + return arr; + } +} +function hasClass(el, className) { + if (objToArr(el.classList).includes(className)) { + return true; + } else { + return false; + } +} +// Style utilities +function getExistingStyle(el, prop) { + return parseInt(document.defaultView.getComputedStyle(el)[prop], 10); +} +function getExistingWidth(el) { + return getExistingStyle(el, 'width'); +} +function getExistingMaxWidth(el) { + return getExistingStyle(el, 'maxWidth'); +} +function getExistingHeight(el) { + return getExistingStyle(el, 'height'); +} +function getExistingMaxheight(el) { + return getExistingStyle(el, 'maxHeight'); +} +function setFlexBasis(el, flexBasis) { + el.setAttribute('style', 'flex-basis: ' + flexBasis + 'px'); +} diff --git a/dist/template-app/resize-demo.html b/dist/template-app/resize-demo.html new file mode 100644 index 0000000..94dce9a --- /dev/null +++ b/dist/template-app/resize-demo.html @@ -0,0 +1,193 @@ + + + + Photon + + + + + + + + +
+ + +
+

Photon

+
+ + +
+
+ +
+
+
Nest .panes and .pane-groups as deep as you want.
+
Nest .panes and .pane-groups as deep as you want.
+
+
Vertical .pane-groups
+
Click and drag to resize your panes.
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameKindDate ModifiedAuthor
bars.scssDocumentOct 13, 2015connors
base.scssDocumentOct 13, 2015connors
button-groups.scssDocumentOct 13, 2015connors
buttons.scssDocumentOct 13, 2015connors
docs.scssDocumentOct 13, 2015connors
forms.scssDocumentOct 13, 2015connors
grid.scssDocumentOct 13, 2015connors
icons.scssDocumentOct 13, 2015connors
images.scssDocumentOct 13, 2015connors
lists.scssDocumentOct 13, 2015connors
mixins.scssDocumentOct 13, 2015connors
navs.scssDocumentOct 13, 2015connors
normalize.scssDocumentOct 13, 2015connors
photon.scssDocumentOct 13, 2015connors
tables.scssDocumentOct 13, 2015connors
tabs.scssDocumentOct 13, 2015connors
utilities.scssDocumentOct 13, 2015connors
variables.scssDocumentOct 13, 2015connors
+
+
+
+
+
+ + + diff --git a/sass/grid.scss b/sass/grid.scss index 3760057..fca5252 100644 --- a/sass/grid.scss +++ b/sass/grid.scss @@ -2,13 +2,34 @@ // The Grid.css // -------------------------------------------------- -.pane-group { +// Only the root-level .pane-group is fixed positioned +.window-content > .pane-group { position: absolute; top: 0; right: 0; bottom: 0; left: 0; +} + +.pane-group { display: flex; + flex-flow: row nowrap; + justify-content: space-between; + flex: 1; +} + +// Vertical pane groups +.pane-group-vertical { + flex-direction: column; + justify-content: space-between; + // Only apply these styles to direct .pane descendents + > .pane { + border-left: 0; + border-top: 1px solid $border-color; + &:first-child { + border-top: 0; + } + } } .pane { diff --git a/sass/photon.scss b/sass/photon.scss index 0bfecce..b7dfdf6 100644 --- a/sass/photon.scss +++ b/sass/photon.scss @@ -20,5 +20,6 @@ @import "lists.scss"; @import "navs.scss"; @import "icons.scss"; +@import "splitters.scss"; @import "tables.scss"; @import "tabs.scss"; diff --git a/sass/splitters.scss b/sass/splitters.scss new file mode 100644 index 0000000..15ef6b3 --- /dev/null +++ b/sass/splitters.scss @@ -0,0 +1,37 @@ +// +// Splitters.css +// -------------------------------------------------- +$splitter-color: transparent; +$splitter-size: 1px; +$splitter-zone-size: 10px; + +// .splitter and .splitter elements are created automatically in resize.js + +.splitter { + background: $splitter-color; + width: $splitter-size; + position: relative; +} +.splitter-zone { + z-index: 1000; + cursor: col-resize; + position: absolute; + top: 0; + bottom: 0; + left: (-$splitter-zone-size / 2); + right: (-$splitter-zone-size / 2); +} +.splitter-vertical { + background: $splitter-color; + height: $splitter-size; + position: relative; +} +.splitter-zone-vertical { + z-index: 1000; + cursor: row-resize; + position: absolute; + top: (-$splitter-zone-size / 2); + bottom: (-$splitter-zone-size / 2); + left: 0; + right: 0; +}