More Work done

This commit is contained in:
Marc 2015-04-19 21:19:54 +02:00
parent 5e91ff77dc
commit f531220f8d
6 changed files with 469 additions and 176 deletions

View File

@ -26,10 +26,10 @@ class CustomControlPlugin(octoprint.plugin.SettingsPlugin,
return dict( return dict(
js=[ js=[
"js/customControl.js", "js/customControl.js",
"js/containerDialog.js", "js/customControlDialog.js",
], ],
css=["css/customControls.css"], css=["css/customControls.css"],
less=["less/customControls.css"] less=["less/customControls.less"]
) )
# If you want your plugin to be registered within OctoPrint under a different name than what you defined in setup.py # If you want your plugin to be registered within OctoPrint under a different name than what you defined in setup.py

View File

@ -1,33 +0,0 @@
$(function () {
function ContainerDialogViewModel(parameters) {
var self = this;
self.element = ko.observable();
self.layouts = ko.observableArray([
{ name: gettext("Vertical"), key: "vertical" },
{ name: gettext("Horizontal"), key: "horizontal" },
{ name: gettext("Horizontal grid"), key: "horizontal_grid" }
]);
self.show = function (f) {
var dialog = $("#containerDialog");
var primarybtn = $('.btn-primary', dialog);
primarybtn.unbind('click').bind('click', f);
dialog.modal({
show: 'true',
backdrop: 'static',
keyboard: false
});
}
}
// view model class, parameters for constructor, container to bind to
OCTOPRINT_VIEWMODELS.push([
ContainerDialogViewModel,
[],
"#containerDialog"
]);
});

View File

@ -5,7 +5,8 @@
self.loginState = parameters[0]; self.loginState = parameters[0];
self.settingsViewModel = parameters[1]; self.settingsViewModel = parameters[1];
self.controlViewModel = parameters[2]; self.controlViewModel = parameters[2];
self.containerDialogViewModel = parameters[3];
self.customControlDialogViewModel = parameters[3];
self.popup = undefined; self.popup = undefined;
@ -65,12 +66,38 @@
return controls; return controls;
}; };
self._processInput = function (control) {
for (var i = 0; i < control.input.length; i++) {
if (!control.processed) {
control.input[i].value = ko.observable(control.input[i].default);
control.input[i].default = ko.observable(control.input[i].default);
}
if (control.processed)
control.input[i].value(control.input[i].default());
if (!control.input[i].hasOwnProperty("slider"))
control.input[i].slider = ko.observable(false);
else if (!control.processed)
control.input[i].slider = ko.observable(control.input[i].slider);
}
}
self._processControl = function (parent, control) { self._processControl = function (parent, control) {
control.id = ko.observable("settingsCustomControl_id" + self.staticID++); control.id = ko.observable("settingsCustomControl_id" + self.staticID++);
control.parent = parent; control.parent = parent;
if (control.hasOwnProperty("template") && control.hasOwnProperty("key") && control.hasOwnProperty("template_key") && !control.hasOwnProperty("output")) if (control.hasOwnProperty("template") && control.hasOwnProperty("regex")) {
control.output = ko.observable(""); if (control.processed) {
control.template(control.template());
control.regex(control.regex());
}
else {
control.template = ko.observable(control.template);
control.regex = ko.observable(control.regex);
control.output = ko.computed(function () { return control.template(); });
}
}
if (control.hasOwnProperty("children")) { if (control.hasOwnProperty("children")) {
if (control.processed) { if (control.processed) {
@ -87,27 +114,19 @@
control.layout = ko.observable("vertical"); control.layout = ko.observable("vertical");
else else
control.layout = ko.observable(control.layout); control.layout = ko.observable(control.layout);
control.width = ko.observable(control.hasOwnProperty("width") ? control.width : "2");
control.offset = ko.observable(control.hasOwnProperty("offset") ? control.offset : "");
} }
} }
if (!control.processed) {
if (control.hasOwnProperty("name"))
control.name = ko.observable(control.name);
control.width = ko.observable(control.hasOwnProperty("width") ? control.width : "2");
control.offset = ko.observable(control.hasOwnProperty("offset") ? control.offset : "");
}
if (control.hasOwnProperty("input")) { if (control.hasOwnProperty("input")) {
for (var i = 0; i < control.input.length; i++) { self._processInput(control);
if (!control.processed) {
control.input[i].value = ko.observable(control.input[i].default);
control.input[i].default = ko.observable(control.input[i].default);
}
if (control.processed)
control.input[i].value(control.input[i].default());
if (!control.input[i].hasOwnProperty("slider"))
control.input[i].slider = ko.observable(false);
else if (!control.processed)
control.input[i].slider = ko.observable(control.input[i].slider);
}
} }
var js; var js;
@ -173,114 +192,179 @@
return undefined; return undefined;
} }
self.createElement = function (invokedOn, contextParent, selectedMenu) {
if (invokedOn.attr('id') == "base") {
self.customControlDialogViewModel.show(function (ret) {
self.controlsFromServer.push(ret);
self.rerenderControls();
});
}
else {
var parentElement = self.searchElement(self.controlsFromServer, contextParent.attr('id'));
if (parentElement == undefined) {
self._showPopup({
title: gettext("Something went wrong while creating the new Element"),
type: "error"
});
return;
}
self.customControlDialogViewModel.show(function (ret) {
parentElement.children.push(self._processControl(parentElement, ret));
});
}
}
self.deleteElement = function (invokedOn, contextParent, selectedMenu) {
var element = self.searchElement(self.controlsFromServer, contextParent.attr('id'));
if (element == undefined) {
self._showPopup({
title: gettext("Something went wrong while creating the new Element"),
type: "error"
});
return;
}
showConfirmationDialog("", function (e) {
if (element.parent != undefined)
element.parent.children.remove(element);
else {
self.controlsFromServer = _.without(self.controlsFromServer, element);
self.rerenderControls();
}
});
}
self.editElement = function (invokedOn, contextParent, selectedMenu) {
var element = self.searchElement(self.controlsFromServer, contextParent.attr('id'));
if (element == undefined) {
self._showPopup({
title: gettext("Something went wrong while creating the new Element"),
type: "error"
});
return;
}
var title = "Edit Container";
var type = "container";
var data = {
parent: element.parent,
};
if (element.hasOwnProperty("name"))
data.name = element.name();
if (element.hasOwnProperty("layout")) {
data.layout = element.layout();
title = "Edit Container";
type = "container";
}
if (element.hasOwnProperty("command")) {
data.commands = element.command;
title = "Edit Command";
type = "command";
}
if (element.hasOwnProperty("commands")) {
data.commands = element.commands;
title = "Edit Command";
type = "command";
}
if (element.hasOwnProperty("input"))
{
data.input = [];
_.each(element.input(), function (element, index, list) {
data.input[index] = ko.mapping.toJS(element);
});
}
if (element.hasOwnProperty("template")) {
data.template = element.template();
title = "Edit Output";
type = "output";
}
if (element.hasOwnProperty("regex"))
data.regex = element.regex();
if (element.hasOwnProperty("width"))
data.width = element.width();
if (element.hasOwnProperty("offset"))
data.offset = element.offset();
self.customControlDialogViewModel.reset(data);
self.customControlDialogViewModel.title(gettext(title));
self.customControlDialogViewModel.type(type);
self.customControlDialogViewModel.show(function (ret) {
switch (self.customControlDialogViewModel.type()) {
case "container": {
element.name(ret.name);
element.layout(ret.layout);
}
case "command": {
if (ret.hasOwnProperty("name"))
element.name(ret.name);
delete element.command;
delete element.commands;
delete element.input;
if (ret.command != undefined)
element.command = ret.command;
if (ret.commands != undefined)
element.commands = ret.commands;
if (ret.input != undefined) {
element.input = ret.input;
self._processInput(element);
}
break;
}
case "output": {
element.template(ret.template);
element.regex(ret.regex);
}
}
if (element.parent.layout() == "horizontal_grid") {
if (ret.width != undefined && ret.width != "")
element.width(ret.width);
if (ret.offset != undefined && ret.offset != "")
element.offset(ret.offset);
}
});
}
self.controlContextMenu = function (invokedOn, contextParent, selectedMenu) self.controlContextMenu = function (invokedOn, contextParent, selectedMenu)
{ {
switch (selectedMenu.attr('cmd')) { switch (selectedMenu.attr('cmd')) {
case "createContainer": { case "createContainer": {
if (invokedOn.attr('id') == "base") { self.customControlDialogViewModel.reset();
self.containerDialogViewModel.element({ self.customControlDialogViewModel.title(gettext("Create container"));
name: undefined, self.customControlDialogViewModel.type("container");
children:[],
layout: "vertical",
width: "2",
offset: ""
});
self.containerDialogViewModel.show(function (e) { self.createElement(invokedOn, contextParent, selectedMenu);
self.controlsFromServer.push(self.containerDialogViewModel.element());
self.rerenderControls();
});
}
else {
var parentElement = self.searchElement(self.controlsFromServer, contextParent.attr('id'));
if (parentElement == undefined) {
self._showPopup({
title: gettext("Something went wrong while creating the new Element"),
type:"error"
});
return;
}
self.containerDialogViewModel.element({
parent: parentElement,
name: undefined,
children: [],
layout: "vertical",
width: "2",
offset: ""
});
self.containerDialogViewModel.show(function (e) {
parentElement.children.push(self._processControl(parentElement, self.containerDialogViewModel.element()));
});
}
break; break;
} }
case "editContainer": { case "createCommand": {
var element = self.searchElement(self.controlsFromServer, contextParent.attr('id')); self.customControlDialogViewModel.reset();
if (element == undefined) { self.customControlDialogViewModel.title(gettext("Create Command"));
self._showPopup({ self.customControlDialogViewModel.type("command");
title: gettext("Something went wrong while creating the new Element"),
type: "error"
});
return;
}
var dialog = $('#containerDialog'); self.createElement(invokedOn, contextParent, selectedMenu);
var primarybtn = $('.btn-primary', dialog);
var el = {
parent: element.parent,
name: ko.observable(element.name()),
layout: ko.observable(element.layout()),
width: ko.observable("2"),
offset: ko.observable("")
};
if (element.hasOwnProperty("width"))
el.width(element.width());
if (element.hasOwnProperty("offset"))
el.offset(element.offset());
self.containerDialogViewModel.element(el);
primarybtn.unbind('click').bind('click', function (e) {
var ele = self.containerDialogViewModel.element();
element.name(ele.name());
element.layout(ele.layout());
if (ele.parent.layout() == "horizontal_grid") {
if (ele.width() != undefined)
element.width(ele.width());
if (ele.offset() != undefined)
element.offset(ele.offset());
}
});
dialog.modal({
show: 'true',
backdrop: 'static',
keyboard: false
});
break; break;
} }
case "deleteContainer": { case "createOutput": {
var element = self.searchElement(self.controlsFromServer, contextParent.attr('id')); self.customControlDialogViewModel.reset();
if (element == undefined) { self.customControlDialogViewModel.title(gettext("Create Output"));
self._showPopup({ self.customControlDialogViewModel.type("output");
title: gettext("Something went wrong while creating the new Element"),
type: "error"
});
return;
}
showConfirmationDialog("", function (e) { self.createElement(invokedOn, contextParent, selectedMenu);
if (element.parent != undefined) break;
element.parent.children.remove(element); }
else { case "deleteElement": {
self.controlsFromServer = _.without(self.controlsFromServer, element); self.deleteElement(invokedOn, contextParent, selectedMenu);
self.rerenderControls(); break;
} }
}); case "editElement": {
self.editElement(invokedOn, contextParent, selectedMenu);
break;
} }
} }
} }
@ -299,6 +383,7 @@
delete list[i].id; delete list[i].id;
delete list[i].parent; delete list[i].parent;
delete list[i].processed; delete list[i].processed;
delete list[i].output;
if (list[i].hasOwnProperty("children")) if (list[i].hasOwnProperty("children"))
self.recursiveDeleteProperties(list[i].children()); self.recursiveDeleteProperties(list[i].children());
@ -317,7 +402,7 @@
// view model class, parameters for constructor, container to bind to // view model class, parameters for constructor, container to bind to
OCTOPRINT_VIEWMODELS.push([ OCTOPRINT_VIEWMODELS.push([
CustomControlViewModel, CustomControlViewModel,
["loginStateViewModel", "settingsViewModel", "controlViewModel", "containerDialogViewModel"], ["loginStateViewModel", "settingsViewModel", "controlViewModel", "customControlDialogViewModel"],
"#settings_plugin_octoprint_customControl" "#settings_plugin_octoprint_customControl"
]); ]);
}); });

View File

@ -0,0 +1,169 @@
$(function () {
function customControlDialogViewModel(parameters) {
var self = this;
self.element = ko.observable(ko.mapping.fromJS({
name: undefined,
commands: "",
script: "",
javascript: "",
enabled: "",
children: [],
input: [],
layout: "vertical",
regex: "",
template: "",
confirm: "",
width: "2",
offset: "",
parent: undefined
}));
self.title = ko.observable(gettext("Create Container"));
self.type = ko.observable("container");
self.useInputs = ko.observable(false);
self.useJavaScript = ko.observable(false);
self.useEnabled = ko.observable(false);
self.layouts = ko.observableArray([
{ name: gettext("Vertical"), key: "vertical" },
{ name: gettext("Horizontal"), key: "horizontal" },
{ name: gettext("Horizontal grid"), key: "horizontal_grid" }
]);
self.types = ko.observableArray([
{ name: gettext("Container"), key: "container" },
{ name: gettext("Command"), key: "command" },
{ name: gettext("Output"), key: "output" }
]);
self.hasSlider = ko.computed(function () {
if (self.element() == undefined || self.element().input == undefined)
return false;
_.each(self.element().input(), function (element, index, list) {
if (element.hasOwnProperty("slider"))
return true;
});
return false;
});
self.span = function(parameter) {
return ko.computed(function () {
if (self.hasSlider())
return "span2";
switch (parameter) {
case "name":
case "parameter":
return "span4";
case "default":
return "span3";
}
return "span2";
});
}
self.reset = function (data) {
var element = {
name: undefined,
commands: "",
script: "",
javascript: "",
enabled: "",
input: [],
layout: "vertical",
regex: "",
template: "",
confirm: "",
width: "2",
offset: "",
parent: undefined
};
if (typeof data == "object")
element = _.extend(element, data);
self.element(ko.mapping.fromJS(element));
}
self.show = function (f) {
var dialog = $("#customControlDialog");
var primarybtn = $('.btn-primary', dialog);
primarybtn.unbind('click').bind('click', function (e) {
var obj = ko.mapping.toJS(self.element());
var el = {};
switch (self.type()) {
case "container": {
el.name = obj.name;
el.layout = obj.layout;
el.children = [];
el.width = obj.width;
el.offset = obj.offset;
break;
}
case "command": {
el.name = obj.name;
if (obj.commands.indexOf('\n') == -1)
el.command = obj.commands;
else
el.commands = obj.commands;
el.width = obj.width;
el.offset = obj.offset;
if (self.useInputs()) {
el.input = [];
_.each(obj.input, function (element, index, list) {
var input = {
name: element.name,
parameter: element.parameter,
default: element.default
};
if (element.hasOwnProperty("slider")) {
input["slider"] = {
};
if (element.slider.hasOwnProperty("min") && element.slider.min != "")
input.slider.min = element.slider.min;
if (element.slider.hasOwnProperty("max") && element.slider.max != "")
input.slider.max = element.slider.max;
if (element.slider.hasOwnProperty("step") && element.slider.step != "")
input.slider.step = element.slider.step;
}
});
}
break;
}
case "output": {
el.template = obj.template;
el.regex = obj.regex;
el.width = obj.width;
el.offset = obj.offset;
break;
}
}
f(el);
});
dialog.modal({
show: 'true',
backdrop: 'static',
keyboard: false
});
}
}
// view model class, parameters for constructor, container to bind to
OCTOPRINT_VIEWMODELS.push([
customControlDialogViewModel,
[],
"#customControlDialog"
]);
});

View File

@ -1,21 +1,92 @@
<div id="containerDialog" class="modal hide"> <div id="customControlDialog" class="modal hide">
<div class="modal-header"> <div class="modal-header">
<h3>{{ _('Container Dialog') }}</h3> <h3 data-bind="text: title"></h3>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form class="form-horizontal" data-bind="with: element"> <form class="form-horizontal" data-bind="with: element">
<!-- ko if: $root.type() == "command" || $root.type() == "container" -->
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Name') }}</label> <label class="control-label">{{ _('Name') }}</label>
<div class="controls"> <div class="controls">
<input data-bind="value: name, valueAllowUnset: true"></input> <input data-bind="value: name, valueAllowUnset: true"></input>
</div> </div>
</div> </div>
<!-- /ko -->
<!-- ko if: $root.type() == "container" -->
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Layout') }}</label> <label class="control-label">{{ _('Layout') }}</label>
<div class="controls"> <div class="controls">
<select data-bind="options: $parent.layouts, optionsText: 'name', optionsValue: 'key', value: layout, valueAllowUnset: false"></select> <select data-bind="options: $root.layouts, optionsText: 'name', optionsValue: 'key', value: layout, valueAllowUnset: false"></select>
</div> </div>
</div> </div>
<!-- /ko -->
<!--<div class="control-group">
<label class="control-label">{{ _('Type') }}</label>
<div class="controls">
<select data-bind="options: $root.types, optionsText: 'name', optionsValue: 'key', value: $root.type, valueAllowUnset: false"></select>
</div>
</div>-->
<!-- ko if: $root.type() == "command" -->
<div class="control-group">
<label class="control-label">{{ _('Commands') }}</label>
<div class="controls">
<textarea style="width: 97%; height: auto" data-bind="value: commands"></textarea>
</div>
</div>
<div class="control-group">
<label class="control-label"><input type="checkbox" data-bind="checked: $root.useInputs"></input>{{ _('Use Inputs') }}</label>
</div>
<!-- ko if: $root.useInputs -->
<div class="control-group">
<label class="control-label">{{ _('Inputs') }}</label>
<div class="row-fluid">
<div class="span12">
<div data-bind="css: $root.span('name')">{{ _('Name') }}</div>
<div data-bind="css: $root.span('parameter')">{{ _('Parameter') }}</div>
<div data-bind="css: $root.span('default')">{{ _('Default') }}</div>
<!-- ko if: $root.hasSlider -->
<div class="span2">{{ _('Min') }}</div>
<div class="span2">{{ _('Max') }}</div>
<div class="span2">{{ _('Step') }}</div>
<!-- /ko -->
</div>
</div>
<div data-bind="foreach: input">
<div class="row-fluid" style="margin-bottom: 5px">
<div data-bind="css: $root.span('name')"><input class="span12" data-bind="value: name"></input></div>
<div data-bind="css: $root.span('parameter')"><input class="span12" data-bind="value: parameter"></input></div>
<div data-bind="css: $root.span('default')"><input class="span12" data-bind="value: defaultValue"></input></div>
<!-- ko if: $root.hasSlider -->
<div class="span2"><input class="span12" data-bind="value: slide().min"></input></div>
<div class="span2"><input class="span12" data-bind="value: slide().max"></input></div>
<div class="span1"><input class="span12" data-bind="value: slide().step"></input></div>
<!-- /ko -->
<div class="span1"><a title="Remove Input" class="btn btn-danger" data-bind="click: $root.removeInput($data)"><i class="icon-trash"></i></a></div>
</div>
</div>
<div class="row-fluid">
<div class="offset11 span1"><a title="Add Input" class="btn btn-primary" data-bind="click: $root.addInput"><i class="icon-plus"></i></a></div>
</div>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- ko if: $root.type() == "output" -->
<div class="control-group">
<label class="control-label">{{ _('Regex') }}</label>
<div class="controls">
<input data-bind="value: regex"></input>
</div>
</div>
<div class="control-group">
<label class="control-label">{{ _('Template') }}</label>
<div class="controls">
<textarea style="width: 97%; height: auto" data-bind="value: template"></textarea>
</div>
</div>
<!-- /ko -->
<!-- ko if: $data.parent && $data.parent.layout && $data.parent.layout() == 'horizontal_grid' --> <!-- ko if: $data.parent && $data.parent.layout && $data.parent.layout() == 'horizontal_grid' -->
<div class="control-group"> <div class="control-group">
<label class="control-label">{{ _('Width') }}</label> <label class="control-label">{{ _('Width') }}</label>

View File

@ -7,12 +7,12 @@
<ul id="controlContextMenu" class="dropdown-menu" role="menu" style="display:block;position:fixed !important;margin-bottom:5px;"> <ul id="controlContextMenu" class="dropdown-menu" role="menu" style="display:block;position:fixed !important;margin-bottom:5px;">
<li> <li>
<a href="#" cmd="createContainer">Create Container</a> <a href="#" cmd="createContainer">{{ _('Create Container') }}</a>
</li> </li>
</ul> </ul>
<ul id="commandContextMenu" class="dropdown-menu" role="menu" style="display:block;position:fixed !important;margin-bottom:5px;"> <ul id="commandContextMenu" class="dropdown-menu" role="menu" style="display:block;position:fixed !important;margin-bottom:5px;">
<li><a href="#" cmd="editCommand">Edit Command</a></li> <li><a href="#" cmd="editElement">{{ _('Edit') }}</a></li>
<li><a href="#" cmd="deleteCommand">Delete Command</a></li> <li><a href="#" cmd="deleteElement">{{ _('Delete') }}</a></li>
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-submenu"> <li class="dropdown-submenu">
<a href="#">Style</a> <a href="#">Style</a>
@ -33,13 +33,14 @@
<li class="dropdown-submenu"> <li class="dropdown-submenu">
<a href="#">Create Command</a> <a href="#">Create Command</a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#" cmd="createContainer">Create Container</a></li> <li><a href="#" cmd="createContainer">{{ _('Container') }}</a></li>
<li><a href="#" cmd="createCommand">Command</a></li> <li><a href="#" cmd="createCommand">{{ _('Command') }}</a></li>
<li><a href="#" cmd="createOutput">{{ _('Output') }}</a></li>
</ul> </ul>
</li> </li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="#" cmd="editContainer">Edit Container</a></li> <li><a href="#" cmd="editElement">{{ _('Edit') }}</a></li>
<li><a href="#" cmd="deleteContainer">Delete Container</a></li> <li><a href="#" cmd="deleteElement">{{ _('Delete') }}</a></li>
</ul> </ul>
<!-- Templates for custom controls --> <!-- Templates for custom controls -->
@ -67,7 +68,7 @@
</div> </div>
</script> </script>
<script type="text/html" id="settingsCustomControls_controlTemplate"> <script type="text/html" id="settingsCustomControls_controlTemplate">
<form class="form-inline control-tab custom_control" style="min-height:15px; border:1px solid #000000" data-bind="contextMenu: { menuSelector: '#commandContextMenu', menuSelected: $root.controlContextMenu }, attr: { 'id': id }"> <form class="form-inline control-tab custom_control" style="min-height:15px; border:1px dotted #000000" data-bind="contextMenu: { menuSelector: '#commandContextMenu', menuSelected: $root.controlContextMenu }, attr: { 'id': id }">
<!-- ko template: { name: 'customControls_controlTemplate_input', data: $data, if: $data.hasOwnProperty('input') } --><!-- /ko --> <!-- ko template: { name: 'customControls_controlTemplate_input', data: $data, if: $data.hasOwnProperty('input') } --><!-- /ko -->
<!-- ko template: { name: 'customControls_controlTemplate_command', data: $data, if: $data.hasOwnProperty('command') || $data.hasOwnProperty('commands') || $data.hasOwnProperty('script') || $data.hasOwnProperty('javascript') } --><!-- /ko --> <!-- ko template: { name: 'customControls_controlTemplate_command', data: $data, if: $data.hasOwnProperty('command') || $data.hasOwnProperty('commands') || $data.hasOwnProperty('script') || $data.hasOwnProperty('javascript') } --><!-- /ko -->
<!-- ko template: { name: 'customControls_controlTemplate_output', data: $data, if: $data.hasOwnProperty('output') } --><!-- /ko --> <!-- ko template: { name: 'customControls_controlTemplate_output', data: $data, if: $data.hasOwnProperty('output') } --><!-- /ko -->
@ -88,6 +89,6 @@
<label data-bind="text: output"></label> <label data-bind="text: output"></label>
</script> </script>
<script type="text/html" id="customControls_controlTemplate_command"> <script type="text/html" id="customControls_controlTemplate_command">
<button class="btn" data-bind="text: name, enable: $root.isCustomEnabled($data), click: function() { $root.clickCustom($data) }"></button> <button class="btn" data-bind="text: name }"></button>
</script> </script>
<!-- End of templates for custom controls --> <!-- End of templates for custom controls -->