diff --git a/Packages/design/styles/components/selectTree.styl b/Packages/design/styles/components/selectTree.styl
index 62fbd8bd9..f65135ec0 100644
--- a/Packages/design/styles/components/selectTree.styl
+++ b/Packages/design/styles/components/selectTree.styl
@@ -8,13 +8,53 @@ $gray4 = #C0C0C0
$gray5 = #9B9B9B
$gray6 = #303030
-.select-tree-root
+.select-tree
background-color: white
+
+ &, a.tree-back, .tree-breadcrumb span, .tree-node, .tree-leaf
+ transition(all 0.3s ease)
+
+.select-tree-root
+ &>*:first-child
+ transition(all 0.3s ease)
+
+.select-tree-root.selected
+ border-color: transparent
+ border-radius: 0
+ // height: 26px
+
+ &, .select-tree
+ background-color: transparent
+
+ &:not(.open)>.tree-options
+ &>.tree-inputs, &>.tree-breadcrumb
+ display: none
+
+ &.open>.tree-options
+ &>.tree-inputs>label:not(.active), &>.tree-breadcrumb a, &>.tree-breadcrumb span
+ color: transparent
+ &>.tree-inputs>label.tree-node:not(.active)
+ border-color: transparent
+
+ .tree-search
+ opacity: 0
+
+ &>.tree-options
+ background-color: transparent
+
+ label.tree-leaf.active
+ box-shadow: 0 0 0 10px white, inset 0 0 0 200px white
+
+.select-tree-root
border: 1px solid $gray3
border-radius: 5px
- display: table
+ display: inline-block
overflow: hidden
+ &>.tree-options
+ transform(scale(1))
+ transition(height 0.3s ease)
+
.tree-search
background-color: $gray4
border-bottom: 1px solid $gray3
@@ -36,12 +76,32 @@ $gray6 = #303030
line-height: 32px
padding: 5px 12px
+ .path-link, .tree-current-node
+ display: inline-block
+ font-weight: normal
+ float: left
+ overflow: hidden
+ text-overflow: ellipsis
+ white-space: nowrap
+
+ i
+ margin-right: 5px
+
+ .select-tree-node
+ left: 0
+ position: fixed
+ width: 100%
+ z-index: 2
+
.tree-options
overflow: hidden
+ position: relative
+ z-index: 2
.tree-node, .tree-leaf
cursor: pointer
display: block
+ font-weight: normal
margin-bottom: 0
padding: 0 12px
position: relative
@@ -61,12 +121,13 @@ $gray6 = #303030
border-bottom: 0
.tree-leaf
+ box-shadow: 0 0 0 200px transparent, inset 0 0 0 200px transparent
line-height: 26px
- label
- transition(all 0.3s ease)
+ &.active
+ box-shadow: 0 0 0 200px white, inset 0 0 0 200px white
+ transition(box-shadow 0.3s ease)
+ z-index: 0
&.collapsed
-
- &>.tree-breadcrumb, &>label:not(.active)
- display: none
+ z-index: 2
diff --git a/Packages/design/styles/imports/mixins.styl b/Packages/design/styles/imports/mixins.styl
index c67ced44a..69da48647 100644
--- a/Packages/design/styles/imports/mixins.styl
+++ b/Packages/design/styles/imports/mixins.styl
@@ -1,3 +1,10 @@
+animation(a)
+ -webkit-animation: a
+ -moz-animation: a
+ -ms-animation: a
+ -o-animation: a
+ animation: a
+
transition(t)
transition: t
-webkit-transition: t
diff --git a/Packages/ohif-core/components/base/mixins/selectTree.js b/Packages/ohif-core/components/base/mixins/selectTree.js
index fa01c28ee..55011d574 100644
--- a/Packages/ohif-core/components/base/mixins/selectTree.js
+++ b/Packages/ohif-core/components/base/mixins/selectTree.js
@@ -2,7 +2,6 @@ import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { _ } from 'meteor/underscore';
-import { $ } from 'meteor/jquery';
/*
* input: controls a tree selection component
@@ -13,6 +12,8 @@ OHIF.mixins.selectTree = new OHIF.Mixin({
onCreated() {
const instance = Template.instance();
const component = instance.component;
+ const rootComponent = instance.data.root || component;
+ const rootInstance = rootComponent.templateInstance;
// Create the component's value storage property
instance.data.currentNode = new ReactiveVar(null);
@@ -41,49 +42,6 @@ OHIF.mixins.selectTree = new OHIF.Mixin({
// Change the current node
instance.data.currentNode.set(node);
};
- },
-
- events: {
- 'change .select-tree:first>.tree-options>label>input'(event, instance) {
- const component = instance.component;
- const $target = $(event.target);
- const eventComponent = $target.data('component');
-
- // Change the active item
- const rootComponent = instance.data.root || component;
- rootComponent.templateInstance.$('label').removeClass('active');
- $target.closest('label.tree-leaf').addClass('active');
-
- // Change the component's value
- component.value(eventComponent.value());
- },
-
- 'click .select-tree:first>.tree-options>.tree-breadcrumb>.tree-back'(event, instance) {
- // Prevent the default element action
- event.preventDefault();
-
- // Reset the checked inputs to unchecked state
- const rootComponent = instance.data.root || instance.component;
- rootComponent.templateInstance.$('input').removeAttr('checked');
-
- // Get the index of the breadcrumb's clicked option
- const index = $(event.currentTarget).attr('data-index') | 0;
-
- // Set the current instance
- let currentInstance = instance.component.parent.templateInstance;
-
- // Iterate over all parents until gets into the clicked node
- for (let i = 1; i <= index; i++) {
- // Unset the selected node
- currentInstance.data.currentNode.set(null);
-
- // Check if it's not the root node
- if (i < index) {
- // Change the current instance
- currentInstance = currentInstance.component.parent.templateInstance;
- }
- }
- }
}
}
});
diff --git a/Packages/ohif-core/components/bootstrap/index.js b/Packages/ohif-core/components/bootstrap/index.js
index 186f88253..48677b019 100644
--- a/Packages/ohif-core/components/bootstrap/index.js
+++ b/Packages/ohif-core/components/bootstrap/index.js
@@ -6,3 +6,4 @@ import './input/radio.html';
import './input/select.html';
import './input/text.html';
import './input/selectTree.html';
+import './input/selectTree.js';
diff --git a/Packages/ohif-core/components/bootstrap/input/selectTree.html b/Packages/ohif-core/components/bootstrap/input/selectTree.html
index 16b7b1f04..80ca4ea9d 100644
--- a/Packages/ohif-core/components/bootstrap/input/selectTree.html
+++ b/Packages/ohif-core/components/bootstrap/input/selectTree.html
@@ -20,30 +20,32 @@
{{#if this.root}}
{{>selectTreeBreadcrumb component=this.component}}
{{/if}}
- {{#each item in (reactive this.items)}}
- {{>inputRadio
- class=this.radioClass
- name=this.key
- value=item.value
- label=item.label
- labelClass=(valueIf (isArray item.items) 'tree-node' 'tree-leaf')
- checked=(eq item.value (reactive this.value))
- }}
- {{/each}}
+
+ {{#each item in (reactive this.items)}}
+ {{>inputRadio
+ class=this.radioClass
+ name=this.key
+ value=item.value
+ label=item.label
+ labelClass=(valueIf (isArray item.items) 'tree-node' 'tree-leaf')
+ checked=(eq item.value (reactive this.value))
+ }}
+ {{/each}}
+
+ {{#if (and currentNode (isArray currentNode.items))}}
+ {{>selectTree (clone this
+ items=currentNode.items
+ root=(choose this.root this.component)
+ class='select-tree-node'
+ )}}
+ {{/if}}
- {{#if (and currentNode (isArray currentNode.items))}}
- {{>selectTree (clone this
- items=currentNode.items
- root=(choose this.root this.component)
- class='select-tree-node'
- )}}
- {{/if}}
{{/let}}
{{/group}}
-
+
{{>selectTreeBreadcrumbParent
component=this.component.parent
index=1
@@ -62,11 +64,17 @@
component=parent
index=(sum this.index 1)
}}
-
{{parentNode.label}}
-
/
- {{else}}
-
{{/if}}
+
+
+ {{#if parentNode}}
+ {{parentNode.label}}
+ {{else}}
+
+ {{/if}}
+
+ {{#if parentNode}}/{{/if}}
+
{{/let}}
{{/let}}
diff --git a/Packages/ohif-core/components/bootstrap/input/selectTree.js b/Packages/ohif-core/components/bootstrap/input/selectTree.js
new file mode 100644
index 000000000..799c07046
--- /dev/null
+++ b/Packages/ohif-core/components/bootstrap/input/selectTree.js
@@ -0,0 +1,145 @@
+import { Meteor } from 'meteor/meteor';
+import { Template } from 'meteor/templating';
+import { _ } from 'meteor/underscore';
+import { $ } from 'meteor/jquery';
+
+// TODO: use npm dependency
+import transition from 'meteor/ohif:core/lib/third-party/transition-to-from-auto';
+
+Template.selectTree.onRendered(() => {
+ const instance = Template.instance();
+ const component = instance.$('.select-tree:first').data('component');
+ const rootComponent = instance.data.root || component;
+ const rootInstance = rootComponent.templateInstance;
+
+ instance.component = component;
+
+ // Update the component's viewport height
+ instance.updateHeight = () => {
+ const height = rootInstance.$('.tree-options:last').data('height');
+ rootInstance.$('.tree-options:first').height(height);
+ };
+
+ // Update the opened node
+ instance.updateOpen = () => {
+ rootInstance.$('.select-tree').removeClass('open');
+ rootInstance.$('.select-tree:last').addClass('open');
+ };
+
+ // Put the component in selected state
+ instance.setSelected = (flag) => {
+ const $treeRoot = rootInstance.$('.select-tree-root:first').first();
+ const $firstChild = $treeRoot.children().first();
+ if (flag) {
+ const search = rootInstance.$('.tree-search').outerHeight() | 0;
+ const leaf = $treeRoot.find('.tree-leaf.active').position().top;
+ const martinTop = search + leaf;
+ // $firstChild.css('margin-top', -martinTop);
+ // $treeRoot.css('margin-top', martinTop).addClass('selected');
+ $treeRoot.addClass('selected');
+ } else {
+ // $treeRoot.removeClass('selected').css('margin-top', '');
+ // $firstChild.css('margin-top', '');
+ $treeRoot.removeClass('selected');
+ }
+ };
+
+ // Store the element's initial height
+ const $treeOptions = instance.$('.tree-options:first').first();
+ const height = $treeOptions.height();
+ $treeOptions.data('height', height);
+
+ instance.autorun(() => {
+ // Run this computation everytime the current node is changed
+ instance.data.currentNode.dep.depend();
+
+ // Update the viewport height
+ Tracker.afterFlush(() => {
+ instance.updateHeight();
+ instance.updateOpen();
+ });
+ });
+
+ const selector = '.select-tree:first>.tree-options>.tree-breadcrumb .path-link';
+ instance.$(selector).css('width', 0).each((index, element) => {
+ transition({
+ element: element,
+ prop: 'width',
+ style: 'width 0.3s ease',
+ val: 'auto'
+ });
+ });
+});
+
+Template.selectTree.events({
+ 'change .select-tree:first>.tree-options>.tree-inputs>label>input'(event, instance) {
+ const component = instance.component;
+ const $target = $(event.target);
+ const $label = $target.closest('label');
+ const eventComponent = $target.data('component');
+ const rootComponent = instance.data.root || component;
+ const rootInstance = rootComponent.templateInstance;
+
+ // Change the component's value
+ component.value(eventComponent.value());
+
+ // Unset the active leaf
+ rootInstance.$('label').removeClass('active');
+
+ // Unset the selected state
+ instance.setSelected(false);
+
+ // Check if the clicked element is a node or a leaf
+ const isLeaf = $label.hasClass('tree-leaf');
+ if (isLeaf) {
+ // Change the active item
+ $label.addClass('active');
+
+ // Mark the component as selected
+ instance.setSelected(true);
+ } else {
+ // Get the position of the clicked element
+ const position = $label.position();
+
+ Meteor.defer(() => {
+ const $treeNode = $label.parent().siblings('.select-tree');
+
+ // Do the transition from the clicked position to top
+ $treeNode.css(position);
+ setTimeout(() => $treeNode.css('top', 0));
+ });
+ }
+ },
+
+ 'click .select-tree:first>.tree-options>.tree-breadcrumb .tree-back'(event, instance) {
+ const rootComponent = instance.data.root || instance.component;
+ const rootInstance = rootComponent.templateInstance;
+
+ // Prevent the default element action
+ event.preventDefault();
+
+ // Reset the checked inputs to unchecked state
+ rootInstance.$('input').removeAttr('checked');
+
+ // Unset the selected state
+ instance.setSelected(false);
+
+ // Get the index of the breadcrumb's clicked option
+ const index = $(event.currentTarget).attr('data-index') | 0;
+
+ // Set the current instance
+ let currentInstance = instance.component.parent.templateInstance;
+
+ // Iterate over all parents until gets into the clicked node
+ for (let i = 1; i <= index; i++) {
+ // Unset the selected node
+ currentInstance.data.currentNode.set(null);
+
+ // Check if it's not the root node
+ if (i < index) {
+ // Change the current instance
+ currentInstance = currentInstance.component.parent.templateInstance;
+ }
+ }
+ }
+});
diff --git a/Packages/ohif-core/lib/third-party/transition-to-from-auto.js b/Packages/ohif-core/lib/third-party/transition-to-from-auto.js
new file mode 100644
index 000000000..a32fd8603
--- /dev/null
+++ b/Packages/ohif-core/lib/third-party/transition-to-from-auto.js
@@ -0,0 +1,151 @@
+/*!
+ * transition-to-from-auto 0.5.2
+ * https://github.com/75lb/transition-to-from-auto
+ * Copyright 2015 Lloyd Brookes <75pound@gmail.com>
+ */
+
+/**
+@module
+@alias transition
+*/
+(function(window, document){
+ "use strict";
+
+ var getComputedStyle = window.getComputedStyle;
+ var isTransition = "data-ttfaInTransition";
+
+ var elements = [];
+ var data = [];
+
+ // Transition detecting
+ var transitionProp = false;
+ var transitionEnd = false;
+ var testStyle = document.createElement("a").style;
+ var testProp;
+
+ if(testStyle[testProp = "webkitTransition"] !== undefined) {
+ transitionProp = testProp;
+ transitionEnd = testProp + "End";
+ }
+
+ if(testStyle[testProp = "transition"] !== undefined) {
+ transitionProp = testProp;
+ transitionEnd = testProp + "end";
+ }
+
+ function process(options, data) {
+ var el = options.element;
+ var val = options.val;
+ var prop = options.prop;
+ var style = el.style;
+ var startVal;
+ var autoVal;
+
+ if(!transitionProp) {
+ return style[prop] = val;
+ }
+
+ if(el.hasAttribute(isTransition)) {
+ el.removeEventListener(transitionEnd, data.l);
+ } else {
+ style[transitionProp] = "none";
+
+ startVal = getComputedStyle(el)[prop];
+ style[prop] = "auto";
+ autoVal = getComputedStyle(el)[prop];
+
+ // Interrupt
+ if(startVal === val || val === "auto" && startVal === autoVal) {
+ return;
+ }
+
+ data.auto = autoVal;
+ el.setAttribute(isTransition, 1);
+
+ // Transition
+ style[prop] = startVal;
+ el.offsetWidth;
+ style[transitionProp] = options.style;
+ }
+
+ style[prop] = val === "auto" ? data.auto : val;
+
+ data.l = function (e) {
+ if(e.propertyName === prop) {
+ el.removeAttribute(isTransition);
+ el.removeEventListener(transitionEnd, data.l);
+ if(val === "auto") {
+ /* avoid transition flashes in Safari */
+ style[transitionProp] = "none";
+ style[prop] = val;
+ }
+ }
+ };
+
+ el.addEventListener(transitionEnd, data.l);
+ }
+
+ /**
+ @param options {Object}
+ @param options.element {string | element} - The DOM element or selector to transition
+ @param options.val {string} - The value you want to transition to
+ @param [options.prop] {string} - The CSS property to transition, defaults to `"height"`
+ @param [options.style] {string} - The desired value for the `transition` CSS property (e.g. `"height 1s"`). If specified, this value is added inline and will override your CSS. Leave this value blank if you already have it defined in your stylesheet.
+ @alias module:transition-to-from-auto
+ */
+ function transition(options){
+ var element = options.element;
+ var datum;
+ var index;
+
+ if(typeof element === "string") {
+ element = document.querySelector(element);
+ }
+
+ element = options.element = element instanceof Node ? element : false;
+ options.prop = options.prop || "height";
+ options.style = options.style || "";
+
+ if(element) {
+ index = elements.indexOf(element);
+ if(~index) {
+ datum = data[index];
+ } else {
+ datum = {};
+ elements.push(element);
+ data.push(datum);
+ }
+
+ process(options, datum);
+ }
+ }
+
+ /**
+ The name of the vendor-specific transition CSS property
+ @type {string}
+ @example
+ el.style[transition.prop + 'Duration'] = '1s';
+ */
+ transition.prop = transitionProp;
+
+ /**
+ * The name of the [transition end event](https://developer.mozilla.org/en-US/docs/Web/Events/transitionend) in the current browser (typically `"transitionend"` or `"webkitTransitionEnd"`)
+ * @type {string}
+ * @example
+ * el.addEventListener(transition.end, function(){
+ * // the transition ended..
+ * });
+ */
+ transition.end = transitionEnd;
+
+
+ if (typeof module === "object" && module.exports){
+ module.exports = transition;
+ } else if (typeof define === "function" && define.amd){
+ define(function(){
+ return transition;
+ });
+ } else {
+ window.transition = transition;
+ }
+})(window, document);