LT-275: Creating measure flow component common section

This commit is contained in:
Bruno Alves de Faria committed 2016-08-03 11:13:48 -03:00
1 parent 9a8eaff560
commit bbb7dad905
60 files changed
+391 -156

No files matched your search

@@ -0,0 +1,49 @@
import { OHIF } from 'meteor/ohif:core';
/*
* Base component to template instances of all dynamic components
*/
class Component {
// Set up the component
constructor(templateInstance) {
// Store the component in the current view
templateInstance.view._component = this;
// Create an object to register section's content
templateInstance.sections = {};
// Store the template instance in the component
this.templateInstance = templateInstance;
// Store the component's registered sub-components
this.registeredItems = new Set();
}
// Self register the component in its first parent component
registerSelf() {
const parent = OHIF.blaze.getParentComponent(this.templateInstance.view);
if (parent) {
// Store this component's parent in a property
this.parent = parent;
// Add this component in its parent's registered items list
parent.registeredItems.add(this);
}
}
// Self unregister the component in its first parent component
unregisterSelf() {
const parent = OHIF.blaze.getParentComponent(this.templateInstance.view);
if (parent) {
// Remove the parent property from this component
delete this.parent;
// Remove this component from its parent's registered items list
parent.registeredItems.delete(this);
}
}
}
OHIF.Component = Component;
@@ -0,0 +1,27 @@
// Core files
import './component.js';
import './mixin.js';
import './template.js';
// Mixins
import './mixins/component.js';
import './mixins/form.js';
import './mixins/formItem.js';
import './mixins/group.js';
import './mixins/groupRadio.js';
import './mixins/input.js';
import './mixins/schemaData.js';
import './mixins/select.js';
import './mixins/select2.js';
import './mixins/selectTree.js';
// Templates
import './templates/custom.html';
import './templates/div.html';
import './templates/form.html';
import './templates/input.html';
import './templates/select.html';
// wrappers
import './wrappers/label.html';
import './wrappers/title.html';
@@ -0,0 +1,126 @@
import { OHIF } from 'meteor/ohif:core';
import { _ } from 'meteor/underscore';
// Create an object to store all the application mixins
OHIF.mixins = {};
// Class to manage new mixins and its dependencies
class Mixin {
// Create the mixin instance
constructor({ dependencies, composition }) {
// Store the mixin dependencies
this.dependencies = dependencies || '';
// Store the mixin composition
this.composition = composition;
}
// Initialize the mixin applying all its composition functions
init(template, data, applied, behaviors) {
const dependenciesArray = this.dependencies.split(' ');
_.each(dependenciesArray, dependency => {
// Go to next dependency if the current dependency string is blank
if (!dependency) {
return;
}
// Get the dependent mixin to be initizalized
const mixin = Mixin.getMixin(dependency);
// Initizalize the mixin dependencies recursively
mixin.init(template, data, applied, behaviors);
});
// Apply the mixin's composition behaviors to the template
this.apply(template, data, applied, behaviors);
}
// Add the mixin's composition behaviors to the template
apply(template, data, applied, behaviors) {
// Ignore if the mixin was already applied to the template
if (_.contains(applied, this)) {
return;
}
// Store the mixin's composition
const composition = this.composition;
// Iterate over each behavior
_.each(behaviors, behavior => {
// Execute something only after all the mixins are done
let functionName = behavior;
if (functionName === 'onMixins') {
functionName = 'onRendered';
}
if (behavior === 'onData' && composition[behavior]) {
// If it's just data manipulation, call it immediately
composition[behavior](data);
} else if (composition[behavior]) {
// Register the behavior in the template
template[functionName](composition[behavior]);
}
});
// Set the current mixin's state as applied
applied.push(this);
}
// Initialize all data manipulation mixins
static initData(data) {
// Split the mixins by space
const mixinsArray = data.mixins.split(' ');
_.each(mixinsArray, mixinName => {
// Ignore blank strings
if (!mixinName) {
return;
}
// Get the current mixin
const mixin = Mixin.getMixin(mixinName);
// Initialize the data manipulation composition
mixin.init(null, data, [], ['onData']);
});
}
// Initialize all the template's mixins
static initAll(template, data) {
// Split the mixins by space
const mixinsArray = data.mixins.split(' ');
_.each(mixinsArray, mixinName => {
// Ignore blank strings
if (!mixinName) {
return;
}
// Get the current mixin
const mixin = Mixin.getMixin(mixinName);
// Initialize blaze default compositions
mixin.init(template, data, [], ['onCreated', 'onRendered', 'onDestroyed', 'events', 'helpers']);
// Execute some behaviors after all mixins are applied
mixin.init(template, data, [], ['onMixins']);
});
}
// Get a mixin by name
static getMixin(mixinName) {
// Get the mixin from mixins object
const mixin = OHIF.mixins[mixinName];
// Throw an error if the mixin does not exists
if (!mixin) {
throw new Error(`Mixin ${mixinName} not found.`);
}
// Return the found mixin
return mixin;
}
}
// Store the Mixin class inside the shared OHIF object
OHIF.Mixin = Mixin;
@@ -0,0 +1,16 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
/*
* component: create the base structure to aplly specific component mixins
*/
OHIF.mixins.component = new OHIF.Mixin({
composition: {
onCreated() {
const instance = Template.instance();
// Declare a property that will be shared among all dependent mixins
instance.component = new OHIF.Component(this);
}
}
});
@@ -0,0 +1,50 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
/*
* form: controls a form and its registered inputs
*/
OHIF.mixins.form = new OHIF.Mixin({
dependencies: 'group',
composition: {
onCreated() {
const instance = Template.instance();
const component = instance.component;
// Define the form's data schema
const schema = instance.data.schema;
component.schema = schema && schema.newContext();
// Check if the form data is valid in its schema
component.validate = () => {
// Assume validation result as true
let result = true;
// Return true if there's no data schema defined
if (!component.schema) {
return result;
}
// Iterate over each registered form item and validate it
component.registeredItems.forEach(child => {
const key = child.templateInstance.data.key;
// Change result to false if any form item is invalid
if (key && !child.validate()) {
result = false;
}
});
// Return the validation result
return result;
};
},
onRendered() {
const instance = Template.instance();
const component = instance.component;
// Set the component main and style elements
component.$style = component.$element = instance.$('form:first');
}
}
});
@@ -0,0 +1,169 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { Tracker } from "meteor/tracker";
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
/*
* formItem: create a generic controller for form items
* It may be used to manage all components that belong to forms
*/
OHIF.mixins.formItem = new OHIF.Mixin({
dependencies: 'schemaData',
composition: {
onCreated() {
const instance = Template.instance();
const component = instance.component;
// Create a observer to monitor changed values
component.changeObserver = new Tracker.Dependency();
// Register the component in the parent component
component.registerSelf();
// Declare the component elements that will be manipulated
component.$element = $();
component.$style = $();
component.$wrapper = $();
// Get or set the component's value using jQuery's val method
component.value = value => {
const isGet = _.isUndefined(value);
if (isGet) {
return component.$element.val();
}
component.$element.val(value).trigger('change');
};
// Disable or enable the component
component.disable = isDisable => {
component.$element.prop('disabled', !!isDisable);
};
// Set or unset component's readonly property
component.readonly = isReadonly => {
component.$element.prop('readonly', !!isReadonly);
};
// Show or hide the component
component.show = isShow => {
const method = isShow ? 'show' : 'hide';
component.$wrapper[method]();
};
// Add or remove a state from the component
component.state = (state, flag) => {
component.$wrapper.toggleClass(`state-${state}`, !!flag);
};
// Set the component in error state and display the error message
component.error = errorMessage => {
// Set the component error state
component.state('error', errorMessage);
// Set or remove the error message
if (errorMessage) {
component.$element.trigger('errorin');
component.$wrapper.attr('data-error', errorMessage);
} else {
component.$element.trigger('errorout');
component.$wrapper.removeAttr('data-error', errorMessage);
}
};
// Check if the component value is valid in its form's schema
component.validate = () => {
// Get the component's form
const form = component.parent;
// Get the form's data schema
const schema = form && form.schema;
// Get the current component's key
const key = instance.data.key;
// Return true if validation is not needed
if (!key || !schema || !component.$wrapper.is(':visible')) {
return true;
}
// Create the data document for validation
const document = {
[key]: component.value()
};
// Check if the document validation failed
if (!schema.validateOne(document, key)) {
// Set the component in error state and display the message
component.error(schema.keyErrorMessage(key));
// Return false for validation
return false;
}
// Remove the component error state and message
component.error(false);
// Return true for validation
return true;
};
component.depend = () => {
return component.changeObserver.depend();
};
},
onRendered() {
const instance = Template.instance();
const component = instance.component;
// Set the element to be controlled
component.$element = instance.$(':input').first();
// Set the most outer wrapper element
component.$wrapper = instance.wrapper.$('*').first();
},
onDestroyed() {
const instance = Template.instance();
// Register the component in the parent component
instance.component.unregisterSelf();
},
onMixins() {
const instance = Template.instance();
const component = instance.component;
// If no style element was defined, set it as the element itself
if (!component.$style.length) {
component.$style = component.$element;
}
// Set the component in jQuery data after all mixins are rendered
component.$element.data('component', component);
},
events: {
// Enable reactivity by changing a Tracker.Dependency observer
change(event, instance) {
instance.component.changeObserver.changed();
},
// TODO: [design] remove log, show error box/hint over the wrapper
errorin(event, instance) {
console.log('ERROR when validating component', instance.component);
},
// TODO: [design] hide error box/hint
errorout(event, instance) {
}
}
}
});
@@ -0,0 +1,58 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { _ } from 'meteor/underscore';
/*
* group: controls a group and its registered items
*/
OHIF.mixins.group = new OHIF.Mixin({
dependencies: 'formItem',
composition: {
onCreated() {
const instance = Template.instance();
const component = instance.component;
// Get or set the child components values
component.value = value => {
const isGet = _.isUndefined(value);
if (isGet) {
const result = {};
component.registeredItems.forEach(child => {
const key = child.templateInstance.data.key;
if (key) {
result[key] = child.value();
}
});
return result;
}
const groupValue = typeof value === 'object' ? value : {};
component.registeredItems.forEach(child => {
const key = child.templateInstance.data.key;
const childValue = _.isUndefined(groupValue[key]) ? null : groupValue[key];
child.value(childValue);
});
component.$element.trigger('change');
};
// Disable or enable the component
component.disable = isDisable => {
component.registeredItems.forEach(child => child.disable(isDisable));
};
// Set or unset component's readonly property
component.readonly = isReadonly => {
component.registeredItems.forEach(child => child.readonly(isReadonly));
};
},
onRendered() {
const instance = Template.instance();
const component = instance.component;
// Set the element to be controlled
component.$element = instance.$('.component-group:first');
}
}
});
@@ -0,0 +1,32 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
/*
* groupRadio: controls all the radio inputs inside the group
*/
OHIF.mixins.groupRadio = new OHIF.Mixin({
dependencies: 'group',
composition: {
onCreated() {
const instance = Template.instance();
const component = instance.component;
// Get the selected radio's value or select a radio based on value
component.value = value => {
const isGet = _.isUndefined(value);
const elements = [];
component.registeredItems.forEach(child => elements.push(child.$element[0]));
const $elements = $(elements);
if (isGet) {
return $elements.filter(':checked').val();
}
$elements.filter(`[value='${value}']`).prop('checked', true).trigger('change');
};
}
}
});
@@ -0,0 +1,18 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
/*
* input: controls a basic input
*/
OHIF.mixins.input = new OHIF.Mixin({
dependencies: 'formItem',
composition: {
onRendered() {
const instance = Template.instance();
const component = instance.component;
// Set the element to be controlled
component.$element = instance.$('input:first');
}
}
});
@@ -0,0 +1,98 @@
import { OHIF } from 'meteor/ohif:core';
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { _ } from 'meteor/underscore';
// Helper function to get the component's current schema
const getCurrentSchema = (parentComponent, key) => {
// Get the parent component schema
const schema = parentComponent && parentComponent.schema;
// Stop here if there's no key or schema defined
if (!key || !schema) {
return;
}
// Get the current schema data using component's key
const currentSchema = _.clone(schema._schema[key]);
// Merge the sub-schema properties if it's an array
if (Array.isArray(currentSchema.type())) {
_.extend(currentSchema, schema._schema[key + '.$']);
}
// Return the component's schema definitions
return currentSchema;
};
/*
* schemaData: change the component data based on its form's schema data
*/
OHIF.mixins.schemaData = new OHIF.Mixin({
dependencies: 'component',
composition: {
onData() {
// Get the current template data
const data = Template.currentData();
// Get the parent component
const parent = OHIF.blaze.getParentComponent(Blaze.currentView);
// Get the current schema data using component's key
const currentSchema = getCurrentSchema(parent, data.key);
// Stop here if there's no schema data for current key
if (!currentSchema) {
return;
}
// Use schema's label if it was not defined
if (!data.label) {
data.label = new ReactiveVar(currentSchema.label);
}
// Fill the items if it's an array schema
if (!data.items && Array.isArray(currentSchema.allowedValues)) {
// Initialize the items array
const items = [];
// Get the values and labels arrays from schema
const values = currentSchema.allowedValues;
const labels = currentSchema.valuesLabels || [];
// Iterate the allowed values array
for (let i = 0; i < values.length; i++) {
// Push the current item to the items array
items.push({
value: values[i],
label: labels[i] || values[i]
});
}
// Add the items to a reactive instance
data.items = new ReactiveVar(items);
}
},
onMixins() {
const instance = Template.instance();
const component = instance.component;
// Get the current schema data using component's key
const currentSchema = getCurrentSchema(component.parent, instance.data.key);
// Stop here if there's no schema data for current key
if (!currentSchema) {
return;
}
// Fill the component with its default value after rendering
if (currentSchema.defaultValue) {
component.value(currentSchema.defaultValue);
}
}
}
});
@@ -0,0 +1,18 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
/*
* input: controls a basic select
*/
OHIF.mixins.select = new OHIF.Mixin({
dependencies: 'formItem',
composition: {
onRendered() {
const instance = Template.instance();
const component = instance.component;
// Set the element to be controlled
component.$element = instance.$('select:first');
}
}
});
@@ -0,0 +1,38 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { $ } from 'meteor/jquery';
/*
* input: controls a select2 component
*/
OHIF.mixins.select2 = new OHIF.Mixin({
dependencies: 'select',
composition: {
onRendered() {
const instance = Template.instance();
const component = instance.component;
// Apply the select2 to the component
component.$element.select2(instance.data.options);
// Store the select2 instance to allow its further destruction
component.select2Instance = component.$element.data('select2');
},
onDestroyed() {
const instance = Template.instance();
const component = instance.component;
// Destroy the select2 instance to remove unwanted DOM elements
component.select2Instance.destroy();
},
events: {
'focus .select2-hidden-accessible'(event, instance) {
// Redirect the focus to select2 focus control in case of hidden
// accessible being focused (e.g. clicking on outer label)
$(event.currentTarget).nextAll('.select2:first').find('.select2-selection').focus();
}
}
}
});
@@ -0,0 +1,47 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { _ } from 'meteor/underscore';
/*
* input: controls a tree selection component
*/
OHIF.mixins.selectTree = new OHIF.Mixin({
dependencies: 'group',
composition: {
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);
// Share the component in data property
instance.data.component = component;
// Override the default value method
component.value = value => {
const isGet = _.isUndefined(value);
// Return the current node value
if (isGet) {
const currentNode = instance.data.currentNode.get();
return currentNode ? currentNode.value : null;
}
// get the items array
const items = component.templateInstance.data.items;
// Get the node for the selected value
const node = _.findWhere(items, {
value: value
});
// Change the current node
instance.data.currentNode.set(node);
};
}
}
});
@@ -0,0 +1,106 @@
import { OHIF } from 'meteor/ohif:core';
import { _ } from 'meteor/underscore';
// Create a new custom template for the base component
Template.baseComponent = new Template('baseComponent', () => {});
// Inject some custom behaviors in the view's construction function
Template.baseComponent.constructView = function(contentFunc, elseFunc) {
// Get the data passed to the template
const data = Template.currentData();
// Check the base template. If it's not informed set as the custom base
data.base || (data.base = 'baseCustom');
// Get the base template object
const baseTemplate = Template[data.base];
// Throw an error if the base template does not exists
if (!baseTemplate) {
throw new Error(`Template ${data.base} not found.`);
}
// Declare the template object and name it as base name + 'Component'
const template = OHIF.blaze.cloneTemplate(baseTemplate, data.base + 'Component');
// Extract the render function from the base template
template.renderFunction = baseTemplate.renderFunction;
// Check for the mixins. If it's not informed set the lowest level mixin
const mixins = data.mixins || 'component';
// Init the data manipulation mixins
OHIF.Mixin.initData(data);
// Create and fill a list of wrappers that will enclose the component
const wrappers = [];
if (data.wrappers) {
const wrappersList = data.wrappers.split(' ');
_.each(wrappersList, wrapper => wrapper && wrappers.push(wrapper));
}
// Declare a variable to store the wrapper instances that will be rendered
const wrapperInstances = [];
// Declare the content function to render the component
let contentFunction = () => {
// Create the most inner content function
const innerContentFunction = () => {
// Assign properties to all wrappers after template's creation
template.onCreated(() => {
const instance = Template.instance();
// Assign the template most outer wrapper
instance.wrapper = wrapperInstances[0] || instance;
// Iterate over all wrappers and assign the component to them
wrapperInstances.forEach(wrappeInstance => {
wrappeInstance.component = instance.component;
});
});
// Return the view instance
return template.constructView(contentFunc, elseFunc);
};
// Apply the mixins to the component
OHIF.Mixin.initAll(template, data);
// Return the recursive function for wrappers
return Blaze.With(data, innerContentFunction);
};
let wrapper;
while (wrapper = wrappers.shift()) {
// Get the wrapper template
const wrapperTemplate = Template[wrapper];
// Throw an error if the wrapper template does not exists
if (!wrapperTemplate) {
throw new Error(`Template ${wrapper} not found.`);
}
// Clone the wrapper template to avoid assigning duplicated handlers
const currentTemplate = OHIF.blaze.cloneTemplate(wrapperTemplate);
// Store the child content function to render it inside the wrapper
const childContentFunction = contentFunction;
// Create a function that will enable the recursion for wrappers
const enclosingContentFunction = () => {
// Add the current wrapper's instance to the wrapper instances list
currentTemplate.onCreated(() => wrapperInstances.push(Template.instance()));
// Return the wrapper view instance with its child as content
return currentTemplate.constructView(childContentFunction, elseFunc);
};
// Replace the content function enclosing it recursively
contentFunction = () => {
return Blaze.With(data, enclosingContentFunction);
};
}
return contentFunction(contentFunc, elseFunc);
};
@@ -0,0 +1,3 @@
<template name="baseCustom">
{{>UI.contentBlock}}
</template>
@@ -0,0 +1,9 @@
<template name="baseDiv">
<div
id="{{this.id}}"
class="{{this.class}}"
{{this.tagAttributes}}
>
{{>UI.contentBlock}}
</div>
</template>
@@ -0,0 +1,16 @@
<template name="baseForm">
<form
id="{{this.id}}"
class="{{this.class}}"
name="{{this.name}}"
action="{{this.action}}"
method="{{this.method}}"
autocomplete="{{this.autocomplete}}"
enctype="{{this.enctype}}"
novalidate="{{this.novalidate}}"
target="{{this.target}}"
{{this.tagAttributes}}
>
{{>UI.contentBlock}}
</form>
</template>
@@ -0,0 +1,12 @@
<template name="baseInput">
<input
type="{{choose this.type 'text'}}"
id="{{this.id}}"
class="{{this.class}}"
name="{{this.name}}"
value="{{reactive this.value}}"
placeholder="{{this.placeholder}}"
{{this.tagAttributes}}
>
{{>UI.contentBlock}}
</template>
@@ -0,0 +1,17 @@
<template name="baseSelect">
<select
id="{{this.id}}"
class="{{this.class}}"
name="{{this.name}}"
multiple="{{#if this.multiple}}multiple{{/if}}"
{{this.tagAttributes}}
>
{{>UI.contentBlock}}
{{#each item in (reactive this.items)}}
<option
value="{{item.value}}"
selected="{{#if eq item.value (reactive this.value)}}selected{{/if}}"
>{{item.label}}</option>
{{/each}}
</select>
</template>
@@ -0,0 +1,11 @@
<template name="wrapperLabel">
<label class="wrapperLabel {{this.labelClass}}">
{{#if this.labelAfter}}
{{>UI.contentBlock}}
{{/if}}
<span class="wrapperText">{{reactive this.label}}</span>
{{#unless this.labelAfter}}
{{>UI.contentBlock}}
{{/unless}}
</label>
</template>
@@ -0,0 +1,11 @@
<template name="wrapperTitle">
<div class="wrapperTitle {{this.labelClass}}">
{{#if this.labelAfter}}
{{>UI.contentBlock}}
{{/if}}
<span class="wrapperText">{{reactive this.label}}</span>
{{#unless this.labelAfter}}
{{>UI.contentBlock}}
{{/unless}}
</div>
</template>
@@ -0,0 +1,8 @@
<template name="form">
{{#baseComponent (extend this
base='baseForm'
mixins=(concat 'form ' this.mixins)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}
</template>
@@ -0,0 +1,9 @@
<template name="group">
{{#baseComponent (extend this
class=(concat 'component-group ' this.class)
base='baseDiv'
mixins=(concat 'group ' this.mixins)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}
</template>
@@ -0,0 +1,9 @@
import './form/form.html';
import './form/group.html';
import './input/groupRadio.html';
import './input/radio.html';
import './input/select.html';
import './input/text.html';
import './input/selectTree.html';
import './input/selectTree.js';
@@ -0,0 +1,21 @@
<template name="groupRadio">
{{#group (extend this
class=(concat 'group-radio ' this.class)
mixins=(concat 'groupRadio ' this.mixins)
wrappers=(concat ''
(valueIf reactive this.label 'wrapperTitle ' '')
this.wrappers
)
)}}
{{>UI.contentBlock}}
{{#each item in (reactive this.items)}}
{{>inputRadio
class=this.radioClass
name=this.key
value=item.value
label=item.label
checked=(eq item.value (reactive this.value))
}}
{{/each}}
{{/group}}
</template>
@@ -0,0 +1,15 @@
<template name="inputRadio">
{{#baseComponent (extend this
base='baseInput'
type='radio'
mixins=(concat 'input ' this.mixins)
labelAfter=(valueIf (isUndefined this.labelAfter) true this.labelAfter)
labelClass=(concat 'checkboxLabel ' this.labelClass)
wrappers=(concat ''
(valueIf reactive this.label 'wrapperLabel ' '')
this.wrappers
)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}
</template>
@@ -0,0 +1,18 @@
<template name="inputSelect">
{{#baseComponent (extend this
base='baseSelect'
mixins=(concat 'select2 ' this.mixins)
wrappers=(concat ''
(valueIf reactive this.label 'wrapperLabel ' '')
this.wrappers
)
options=(clone this.options
multiple=(valueIf this.multiple true false)
minimumResultsForSearch=(valueIf this.hideSearch
-1 this.minimumResultsForSearch
)
)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}
</template>
@@ -0,0 +1,93 @@
<template name="selectTree">
{{#group (extend this
class=(concat
'select-tree '
(valueIf this.root '' 'select-tree-root ')
this.class
)
mixins=(concat 'selectTree ' this.mixins)
)}}
{{>UI.contentBlock}}
<div class="tree-content">
{{#let currentNode=this.currentNode.get}}
{{#if (and (not this.root) (ne this.search false))}}
{{>inputText
label=this.label
labelClass='tree-search'
placeholder=(choose this.searchPlaceholder 'Search')
}}
{{/if}}
<div class="tree-options {{#if currentNode}}collapsed{{/if}}">
{{#if this.root}}
{{>selectTreeBreadcrumb component=this.component}}
{{/if}}
<div class="tree-inputs">
{{#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}}
</div>
{{#if (and currentNode (isArray currentNode.items))}}
{{>selectTree (clone this
items=currentNode.items
root=(choose this.root this.component)
class='select-tree-node'
)}}
{{/if}}
</div>
{{/let}}
</div>
{{#unless this.root}}
{{>selectTreeCommon this}}
{{/unless}}
{{/group}}
</template>
<template name="selectTreeCommon">
<div class="select-tree-common">
<div class="content">
<h5 class="title">Common</h5>
</div>
</div>
</template>
<template name="selectTreeBreadcrumb">
<div class="tree-breadcrumb clearfix">
{{>selectTreeBreadcrumbParent
component=this.component.parent
index=1
}}
{{#let currentNode=this.component.parent.templateInstance.data.currentNode.get}}
<span class="tree-current-node">{{currentNode.label}}</span>
{{/let}}
</div>
</template>
<template name="selectTreeBreadcrumbParent">
{{#let parent=this.component.parent}}
{{#let parentNode=(reactive parent.templateInstance.data.currentNode)}}
{{#if parentNode}}
{{>selectTreeBreadcrumbParent
component=parent
index=(sum this.index 1)
}}
{{/if}}
<span class="path-link">
<a href="#" class="tree-back" data-index="{{this.index}}">
{{#if parentNode}}
<span>{{parentNode.label}}</span>
{{else}}
<i class="fa fa-fast-backward"></i>
{{/if}}
</a>
{{#if parentNode}}<span>/</span>{{/if}}
</span>
{{/let}}
{{/let}}
</template>
@@ -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/client/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;
const $treeRoot = rootInstance.$('.select-tree-root:first').first();
instance.component = component;
// Start the component transitions
$treeRoot.addClass('started');
// 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) => {
if (flag) {
$treeRoot.addClass('selected');
} else {
$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-content>.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({
'click .select-tree-root'(event, instance) {
// Detect the first interaction with the component and do the animation
$(event.currentTarget).addClass('interacted');
},
'change .select-tree:first>.tree-content>.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-content>.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;
}
}
}
});
@@ -0,0 +1,13 @@
<template name="inputText">
{{#baseComponent (extend this
base='baseInput'
class=(concat 'form-control ' this.class)
mixins=(concat 'input ' this.mixins)
wrappers=(concat ''
(valueIf reactive this.label 'wrapperLabel ' '')
this.wrappers
)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}
</template>
@@ -0,0 +1,4 @@
import './base';
import './bootstrap';
import './playground/playground.html';
import './playground/playground.js';
@@ -0,0 +1,10 @@
<template name="componentPlayground">
<div class="p-a-3">
{{>selectTree
key='label'
items=instance.items
label='Assign label'
searchPlaceholder='Search labels'
}}
</div>
</template>
@@ -0,0 +1,44 @@
Template.componentPlayground.onCreated(() => {
const instance = Template.instance();
instance.items = [{
label: 'Category 1',
value: 'Category 1',
items: [{
label: 'Subcategory 1.1',
value: 'Subcategory 1.1'
}, {
label: 'Subcategory 1.2',
value: 'Subcategory 1.2',
items: [{
label: 'Subcategory 1.2.1',
value: 'Subcategory 1.2.1'
}, {
label: 'Subcategory 1.2.2',
value: 'Subcategory 1.2.2',
items: [{
label: 'Subcategory 1.2.2.1',
value: 'Subcategory 1.2.2.1'
}, {
label: 'Subcategory 1.2.2.2',
value: 'Subcategory 1.2.2.2'
}]
}]
}]
}, {
label: 'Category 2',
value: 'Category 2',
items: [{
label: 'Subcategory 2.1',
value: 'Subcategory 2.1'
}, {
label: 'Subcategory 2.2',
value: 'Subcategory 2.2'
}, {
label: 'Subcategory 2.3',
value: 'Subcategory 2.3'
}]
}, {
label: 'Category 3',
value: 'Category 3'
}];
});