LT-275: Creating measure flow component common section
This commit is contained in:
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'
|
||||
}];
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
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);
|
||||
});
|
||||
|
||||
// Choose the first truthy value in the given values
|
||||
Template.registerHelper('choose', (...values) => {
|
||||
let result;
|
||||
_.each(_.initial(values, 1), value => {
|
||||
if (result) {
|
||||
return;
|
||||
}
|
||||
|
||||
result = value;
|
||||
});
|
||||
return result;
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
/**
|
||||
* Global Blaze UI helpers to development debugging
|
||||
*/
|
||||
|
||||
// Stop here if it's not development environment
|
||||
if (!Meteor.isDevelopment) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug some value on console
|
||||
Template.registerHelper('debug', (...values) => {
|
||||
console.debug(...values);
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import './blaze.js';
|
||||
import './data.js';
|
||||
import './debug.js';
|
||||
import './logical.js';
|
||||
import './number.js';
|
||||
import './string.js';
|
||||
import './typing.js';
|
||||
@@ -0,0 +1,69 @@
|
||||
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;
|
||||
});
|
||||
|
||||
// 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;
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
/**
|
||||
* Global Blaze UI helpers to work with numeric operations
|
||||
*/
|
||||
|
||||
// Sum all the given numbers
|
||||
Template.registerHelper('sum', (...values) => {
|
||||
let result = 0;
|
||||
_.each(_.initial(values, 1), value => (result += (value | 0)));
|
||||
return result;
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
/**
|
||||
* Global Blaze UI helpers to work with Strings
|
||||
*/
|
||||
|
||||
// Check if the value's type is undefined
|
||||
Template.registerHelper('isUndefined', value => {
|
||||
return _.isUndefined(value);
|
||||
});
|
||||
|
||||
// Check if the value's type is object
|
||||
Template.registerHelper('isObject', value => {
|
||||
return _.isObject(value);
|
||||
});
|
||||
|
||||
// Check if the value is an array instance
|
||||
Template.registerHelper('isArray', value => {
|
||||
return _.isArray(value);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import './lib';
|
||||
import './components';
|
||||
import './helpers';
|
||||
import './ui';
|
||||
|
||||
import './routes.js';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import './blaze.js';
|
||||
import './string.js';
|
||||
import './user.js';
|
||||
@@ -0,0 +1,33 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { $ } from 'meteor/jquery';
|
||||
|
||||
OHIF.string = {};
|
||||
|
||||
// Search for some string inside any object or array
|
||||
OHIF.string.search = (object, query, property=null, result=[]) => {
|
||||
// Create the search pattern
|
||||
const pattern = new RegExp($.trim(query).replace(/\s/gi, '|'), 'i');
|
||||
|
||||
_.each(object, item => {
|
||||
// Stop here if item is empty
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the value to be compared
|
||||
const value = _.isString(property) ? item[property] : item;
|
||||
|
||||
// Check if the value match the pattern
|
||||
if (_.isString(value) && pattern.test(value)) {
|
||||
// Add the current item to the result
|
||||
result.push(item);
|
||||
} else if (_.isObject(value)) {
|
||||
// Search recursively the item if the current item is an object
|
||||
OHIF.string.search(value, query, property, result);
|
||||
}
|
||||
});
|
||||
|
||||
// Return the found items
|
||||
return result;
|
||||
};
|
||||
@@ -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);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
OHIF.user = {};
|
||||
|
||||
// Throw error if there is no user logged in
|
||||
OHIF.user.validate = () => {
|
||||
if (!Meteor.userId()) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
};
|
||||
|
||||
// Get the persistent data by key
|
||||
OHIF.user.getData = key => {
|
||||
// Check if there is an user logged in
|
||||
OHIF.user.validate();
|
||||
|
||||
// Get the user persistent data
|
||||
const data = Meteor.user().profile.persistent;
|
||||
|
||||
if (data) {
|
||||
return data[key];
|
||||
}
|
||||
};
|
||||
|
||||
// Store the persistent data by giving a key and a value to store
|
||||
OHIF.user.setData = (key, value) => {
|
||||
// Check if there is an user logged in
|
||||
OHIF.user.validate();
|
||||
|
||||
// Call the update method on server-side
|
||||
Meteor.call('userDataSet', key, value);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Router } from 'meteor/clinical:router';
|
||||
|
||||
Router.route('/playground', function() {
|
||||
this.render('componentPlayground');
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
|
||||
Reference in new issue
Block a user