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}} 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);