LT-253: Creating dynamic form components

This commit is contained in:
Bruno Alves de Faria authored and Erik Ziegler committed 2016-07-22 11:12:04 +02:00
1 parent 88f737644f
commit cb875b1913
72 files changed
+1169 -126

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,26 @@
// 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';
// 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';
+126
View File
@@ -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,154 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
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: 'component',
composition: {
onCreated() {
const instance = Template.instance();
const component = instance.component;
// 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;
};
},
onRendered() {
const instance = Template.instance();
const component = instance.component;
// Set the element to be controlled
component.$element = instance.$(':input:first');
// Set the element to be styled
component.$style = component.$element;
// 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;
// Set the component in jQuery data after all mixins are rendered
component.$element.data('component', component);
},
events: {
// 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,50 @@
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));
};
}
}
});
@@ -0,0 +1,30 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
import { _ } from 'meteor/underscore';
/*
* 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.add(child.$element));
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,46 @@
import { OHIF } from 'meteor/ohif:core';
import { Blaze } from 'meteor/blaze';
import { Template } from 'meteor/templating';
import { _ } from 'meteor/underscore';
/*
* schemaData: change the component data based on its form's schema data
*/
OHIF.mixins.schemaData = new OHIF.Mixin({
dependencies: 'formItem',
composition: {
onData() {
const data = Template.currentData();
const parent = OHIF.blaze.getParentComponent(Blaze.currentView);
// Get the parent component schema
const schema = parent && parent.schema;
// Get the current component's key
const key = data.key;
// 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 = schema._schema[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);
}
// TODO: [design] convert allowedValues to items and find a way to get key/value pairs
}
}
});
@@ -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,18 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
/*
* input: controls a select2 component
*/
OHIF.mixins.select2 = new OHIF.Mixin({
dependencies: 'select',
composition: {
onRendered() {
const instance = Template.instance();
const component = instance.component;
// Set the element to be controlled
component.$element = instance.$('select:first');
}
}
});
@@ -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 ${data.base} 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,11 @@
<template name="baseInput">
<input
type="{{choose this.type 'text'}}"
id="{{this.id}}"
class="{{this.class}}"
name="{{this.name}}"
value="{{reactive this.value}}"
{{this.tagAttributes}}
>
{{>UI.contentBlock}}
</template>
@@ -0,0 +1,16 @@
<template name="baseSelect">
<select
id="{{this.id}}"
class="{{this.class}}"
name="{{this.name}}"
{{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="wrapperLabelText">{{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="wrapperTitleText">{{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,8 @@
<template name="group">
{{#baseComponent (extend this
base="baseDiv"
mixins=(concat "group " this.mixins)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}
</template>
@@ -0,0 +1,7 @@
import './form/form.html';
import './form/group.html';
import './input/groupRadio.html';
import './input/radio.html';
import './input/select.html';
import './input/text.html';
@@ -0,0 +1,20 @@
<template name="groupRadio">
{{#group (extend this
class=(concat 'form-group ' this.class)
wrappers=(concat ''
this.wrappers
(valueIf reactive this.label 'wrapperTitle ' '')
)
)}}
{{>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,14 @@
<template name="inputRadio">
{{#baseComponent (extend this
base='baseInput'
mixins=(concat 'input ' this.mixins)
labelAfter=(valueIf (isDefined this.labelAfter) this.labelAfter true)
labelClass=(concat 'checkboxLabel' this.labelClass)
wrappers=(concat ''
this.wrappers
(valueIf reactive this.label 'wrapperLabel ' '')
)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}
</template>
@@ -0,0 +1,13 @@
<template name="inputSelect">
{{#baseComponent (extend this
base='baseSelect'
class=(concat 'form-control ' this.class)
mixins=(concat 'select ' this.mixins)
wrappers=(concat ''
this.wrappers
(valueIf reactive this.label 'wrapperLabel ' '')
)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}
</template>
@@ -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 ''
this.wrappers
(valueIf reactive this.label 'wrapperLabel ' '')
)
)}}
{{>UI.contentBlock}}
{{/baseComponent}}
</template>
+2
View File
@@ -0,0 +1,2 @@
import './base';
import './bootstrap';
+26
View File
@@ -0,0 +1,26 @@
import { Template } from 'meteor/templating';
import { Session } from 'meteor/session';
import { ReactiveVar } from 'meteor/reactive-var';
/**
* Global Blaze UI helpers to work with Blaze
*/
// Return the current template instance
Template.registerHelper('instance', () => {
return Template.instance();
});
// Return the session value for the given key
Template.registerHelper('session', key => {
return Session.get(key);
});
// Return the value for given parameter regardless if it's reactive or not
Template.registerHelper('reactive', parameter => {
if (parameter instanceof ReactiveVar) {
return parameter.get();
}
return parameter;
});
+59
View File
@@ -0,0 +1,59 @@
import { _ } from 'meteor/underscore';
import { Template } from 'meteor/templating';
/**
* Global Blaze UI helpers to manipulate data
*/
// Base extend function to be used by extend and clone helpers
const extend = (...argsArray) => {
// Create the resulting object
const result = argsArray[0] || {};
// Extract the Spacebars kw hash
const kwHash = _.last(argsArray).hash;
// Extract the given objects
const objects = _.initial(argsArray);
// Iterate over the given objects
_.each(objects, current => {
// Stop here if the current argument is not an object
if (typeof current !== 'object') {
return;
}
// Extend the resulting object with the current argument object
_.extend(result, current);
});
// Extend the resulting object with the Spacebars kw hash
_.extend(result, kwHash);
// Return the resulting object
return result;
};
// Extend the first argument object it with the other argument objects
Template.registerHelper('extend', (...argsArray) => {
return extend(...argsArray);
});
// Create a new object and extends it with the argument objects
Template.registerHelper('clone', (...argsArray) => {
const newArgs = argsArray.slice();
newArgs.unshift({});
return extend(...newArgs);
});
// Return the first thrut value in the given arguments
Template.registerHelper('choose', (...argsArray) => {
// Iterate over the given objects
for (let i = 0; i < argsArray.length; i++) {
// Check if the current value is truth
if (!!argsArray[i]) {
// Return the current value
return argsArray[i];
}
}
});
+4
View File
@@ -0,0 +1,4 @@
import './blaze';
import './data';
import './logical';
import './string';
+81
View File
@@ -0,0 +1,81 @@
import { _ } from 'meteor/underscore';
import { Template } from 'meteor/templating';
/**
* Global Blaze UI helpers to work with logical operations
*/
// Convert any value into a boolean value
Template.registerHelper('bool', value => {
return !!value;
});
// Check if two values are identical
Template.registerHelper('eq', (a, b) => {
return a === b;
});
// Check if two values are different
Template.registerHelper('ne', (a, b) => {
return a === b;
});
// Check if the first value is greater than the second one
Template.registerHelper('gt', (a, b) => {
return a > b;
});
// Check if the first value is lesser than the second one
Template.registerHelper('lt', (a, b) => {
return a < b;
});
// Check if the first value is greater than or equals the second one
Template.registerHelper('gte', (a, b) => {
return a >= b;
});
// Check if the first value is lesser than or equals the second one
Template.registerHelper('lte', (a, b) => {
return a <= b;
});
// Get the boolean negation for the given value
Template.registerHelper('not', value => {
return !value;
});
// Check if all the given values are true
Template.registerHelper('and', (...values) => {
let result = true;
_.each(_.initial(values, 1), value => value || (result = false));
return result;
});
// Check if one of the given values is true
Template.registerHelper('or', (...values) => {
let result = false;
_.each(_.initial(values, 1), value => value && (result = true));
return result;
});
// Choose the first truthy value in the given values
Template.registerHelper('choose', (...values) => {
let result;
_.each(_.initial(values, 1), value => value && (result = value));
return result;
});
// Return the second parameter if the first is true or the third if it's false
Template.registerHelper('valueIf', (condition, valueIfTrue, valueIfFalse) => {
if (condition) {
return valueIfTrue;
}
return valueIfFalse;
});
// Check if the value is different from undefined
Template.registerHelper('isDefined', value => {
return typeof value !== 'undefined';
});
+16
View File
@@ -0,0 +1,16 @@
import { _ } from 'meteor/underscore';
import { Template } from 'meteor/templating';
/**
* Global Blaze UI helpers to work with Strings
*/
// Concatenate the give strings
Template.registerHelper('concat', (...args) => {
const values = _.initial(args, 1);
let result = '';
_.each(values, value => {
result += value || '';
});
return result;
});
+42
View File
@@ -0,0 +1,42 @@
import { OHIF } from 'meteor/ohif:core';
OHIF.blaze = {};
// Clone a template and return the clone
OHIF.blaze.cloneTemplate = (template, newName) => {
if (!template){
return;
}
const name = newName || template.viewName;
const clone = new Template(name, template.renderFunction);
clone.inheritsEventsFrom(template);
clone.inheritsHelpersFrom(template);
clone.inheritsHooksFrom(template);
return clone;
};
// Navigate upwards the component and get the parent with the given view name
OHIF.blaze.getParentView = (view, parentViewName) => {
let currentView = view;
while (currentView) {
if (currentView.name === parentViewName) {
break;
}
currentView = currentView.originalParentView || currentView.parentView;
}
return currentView;
};
// Search for the parent component of the given view
OHIF.blaze.getParentComponent = (view) => {
let currentView = view;
while (currentView) {
currentView = currentView.originalParentView || currentView.parentView;
if (currentView && currentView._component) {
return currentView._component;
}
}
};
+1
View File
@@ -0,0 +1 @@
import './blaze.js';
+6
View File
@@ -0,0 +1,6 @@
export { OHIF } from './ohif.js';
import './lib';
import './components';
import './helpers';
import './ui';
+11
View File
@@ -0,0 +1,11 @@
/*
* Defines the base OHIF object
*/
const OHIF = {};
if (Meteor.isDevelopment) {
window.OHIF = OHIF;
}
export { OHIF };
+15
View File
@@ -0,0 +1,15 @@
Package.describe({
name: 'ohif:core',
summary: 'OHIF core components, helpers and UI functions',
version: '0.0.1'
});
Package.onUse(function(api) {
api.versionsFrom('1.3.4.1');
api.use('ecmascript');
api.use('standard-app-packages');
api.use('jquery');
api.mainModule('main.js', 'client');
});
@@ -0,0 +1,110 @@
// Allow attaching to jQuery selectors
$.fn.draggable = function() {
makeDraggable(this);
return this;
};
/**
* This function makes an element movable around the page.
* It supports mouse and touch input and allows whichever element
* is specified to be moved to any arbitrary position.
*
* @param element
*/
makeDraggable = function(element) {
var container = $(window);
var diffX,
diffY;
function getCursorCoords(e) {
var cursor = {
x: e.clientX,
y: e.clientY
};
// Handle touchMove cases
if (cursor.x === undefined) {
cursor.x = e.originalEvent.touches[0].pageX;
}
if (cursor.y === undefined) {
cursor.y = e.originalEvent.touches[0].pageY;
}
return cursor;
}
function startMoving(e) {
// Prevent dragging dialog by clicking on slider
// (could be extended for buttons, not sure it's necessary
if (e.target.type && e.target.type === 'range') {
return;
}
var elementTop = parseFloat(element.offset().top),
elementLeft = parseFloat(element.offset().left);
var cursor = getCursorCoords(e);
diffX = cursor.x - elementLeft;
diffY = cursor.y - elementTop;
container.css('cursor', 'move');
element.css('cursor', 'move');
element.css({
cursor: 'move',
left: elementLeft + 'px',
top: elementTop + 'px',
bottom: 'auto', // Setting these to empty doesn't seem to work in Firefox or Safari
right: 'auto'
});
$(document).on('mousemove', moveHandler);
$(document).on('mouseup', stopMoving);
$(document).on('touchmove', moveHandler);
$(document).on('touchend', stopMoving);
}
function stopMoving() {
container.css('cursor', 'default');
element.css('cursor', 'default');
$(document).off('mousemove', moveHandler);
$(document).off('touchmove', moveHandler);
}
function moveHandler(e) {
// Prevent dialog box dragging whole page in iOS
e.preventDefault();
var elementWidth = parseFloat(element.outerWidth()),
elementHeight = parseFloat(element.outerHeight()),
containerWidth = parseFloat(container.width()),
containerHeight = parseFloat(container.height());
var cursor = getCursorCoords(e);
var elementLeft = cursor.x - diffX;
var elementTop = cursor.y - diffY;
elementLeft = Math.max(elementLeft, 0);
elementTop = Math.max(elementTop, 0);
if (elementLeft + elementWidth > containerWidth) {
elementLeft = containerWidth - elementWidth;
}
if (elementTop + elementHeight > containerHeight) {
elementTop = containerHeight - elementHeight;
}
element.css({
left: elementLeft + 'px',
top: elementTop + 'px',
bottom: 'auto', // Setting these to empty doesn't seem to work in Firefox or Safari
right: 'auto'
});
}
element.on('mousedown', startMoving);
element.on('touchstart', startMoving);
};
+2
View File
@@ -0,0 +1,2 @@
import './draggable/draggable.js';
import './resizable/resizable.js';
@@ -0,0 +1,147 @@
// Allow attaching to jQuery selectors
$.fn.resizable = function(options) {
_.each(this, element => {
const resizableInstance = $(element).data('resizableInstance');
if (options === 'destroy' && resizableInstance) {
$(element).removeData('resizableInstance');
resizableInstance.destroy();
} else {
if (resizableInstance) {
resizableInstance.options(options);
} else {
$(element).data('resizableInstance', new Resizable(element, options));
}
}
});
return this;
};
/**
* This class makes an element resizable.
*/
class Resizable {
constructor(element, options={}) {
this.element = element;
this.$element = $(element);
this.options(options);
}
options(options={}) {
const { boundSize, minWidth, minHeight } = options;
this.minWidth = minWidth || this.$element.width() || 16;
this.minHeight = minHeight || this.$element.height() || 16;
this.boundSize = boundSize || 8;
this.destroy();
this.init();
}
init() {
for (var x = -1; x <= 1; x++) {
for (var y = -1; y <= 1; y++) {
this.createBound(x, y);
}
}
this.$element.addClass('resizable');
}
destroy() {
this.$element.removeClass('resizable').find('resize-bound').remove();
}
attachEventHandlers($bound, xDirection, yDirection) {
const $window = $(window);
const initResizeHandler = event => {
event.stopPropagation();
this.width = this.initialWidth = this.$element.width();
this.height = this.initialHeight = this.$element.height();
this.startWidth = this.width;
this.startHeight = this.height;
this.posX = parseInt(this.$element.css('left'));
this.posY = parseInt(this.$element.css('top'));
this.startPosX = this.posX;
this.startPosY = this.posY;
this.startX = event.clientX;
this.startY = event.clientY;
this.$element.addClass('resizing');
$window.on('mousemove', resizeHandler);
$window.on('mouseup', endResizeHandler);
};
const resizeHandler = event => {
let x, y;
x = event.clientX < 0 ? 0 : event.clientX;
x = x > window.innerWidth ? window.innerWidth : x;
y = event.clientY < 0 ? 0 : event.clientY;
y = y > window.innerHeight ? window.innerHeight : y;
const xDistance = (x - this.startX) * xDirection;
const yDistance = (y - this.startY) * yDirection;
const width = xDistance + this.startWidth;
const height = yDistance + this.startHeight;
this.width = width < this.minWidth ? this.minWidth : width;
this.height = height < this.minHeight ? this.minHeight : height;
this.$element.width(this.width);
this.$element.height(this.height);
if (xDirection < 0) {
this.posX = this.startPosX - xDistance;
if (width < this.minWidth) {
this.posX = this.startPosX + (this.startWidth - this.minWidth);
}
this.$element.css('left', `${this.posX}px`);
}
if (yDirection < 0) {
this.posY = this.startPosY - yDistance;
if (height < this.minHeight) {
this.posY = this.startPosY + (this.startHeight - this.minHeight);
}
this.$element.css('top', `${this.posY}px`);
}
};
const endResizeHandler = event => {
$window.off('mousemove', resizeHandler);
$window.off('mouseup', endResizeHandler);
this.$element.removeClass('resizing');
};
$bound.on('mousedown', initResizeHandler);
}
createBound(xDirection, yDirection) {
if (xDirection === 0 && xDirection === yDirection) {
return;
}
const $bound = $('<div class="resize-bound"></div>');
$bound[0].onselectstart = () => false;
$bound.css('font-size', `${this.boundSize}px`);
const mapX = ['left', 'center', 'right'];
const mapY = ['top', 'middle', 'bottom'];
$bound.addClass('bound-' + mapX[xDirection + 1]);
$bound.addClass('bound-' + mapY[yDirection + 1]);
$bound.appendTo(this.$element);
this.attachEventHandlers($bound, xDirection, yDirection);
}
}
@@ -0,0 +1,55 @@
@import "{design}/app"
.resizable
transform(scale(1))
&.resizing, .resize-bound
user-select: none
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none
-moz-user-select: none
-ms-user-select: none
.resize-bound
content: ''
line-height: 1em
position: fixed
.bound-left
left: 0
width: 1em
.bound-center
left: 1em
height: 1em
right: 1em
.bound-right
right: 0
width: 1em
.bound-top
top: 0
height: 1em
.bound-middle
top: 1em
width: 1em
bottom: 1em
.bound-bottom
bottom: 0
height: 1em
.bound-left.bound-top, .bound-right.bound-bottom
cursor: nwse-resize
.bound-left.bound-bottom, .bound-right.bound-top
cursor: nesw-resize
.bound-left.bound-middle, .bound-right.bound-middle
cursor: ew-resize
.bound-center.bound-top, .bound-center.bound-bottom
cursor: ns-resize