Rewrite <editable> directive

Instead of showing popup for the value being edited it now allows
editing the same value in-place, accompanied with 2 buttons - confirm
& reject the edit. Only when one of 2 these buttons is pressed (or
equivalend Enter/Esc keys are pressed) the value will be applied. The
<editable> directive could be used both for changing Action/Workflow
entity names, for changing dictionary and varlist keys.

Change-Id: I4e7e1c0fcaed71aa649b1c9925a9b53005ff9a2d
Closes-Bug: #1411649
This commit is contained in:
Timur Sufiev 2015-04-27 21:26:49 +03:00
parent e5b8fb8a3a
commit 0efc6355f7
12 changed files with 183 additions and 56 deletions

View File

@ -37,7 +37,7 @@
<div class="right-column">
<div class="form-group">
<label for="elem-{$ $id $}.{$ key $}">
<editable value="key" label="New Name"></editable>
<editable ng-model="value.keyValue" ng-model-options="{getterSetter: true}"></editable>
</label>
<div class="input-group">
<input type="text" id="elem-{$ $id $}.{$ key $}" class="form-control" ng-model="value.value"

View File

@ -63,8 +63,7 @@
<div class="two-panels">
<div class="left-panel">
<panel ng-repeat="panel in workbook | extractPanels track by panel.id"
title="{$ panel.getTitle() $}" removable="{$ panel.removable $}"
on-remove="panel.remove()">
content="panel">
<div ng-repeat="row in panel | extractRows track by row.id">
<div ng-class="{'two-columns': row.index !== undefined }">
<div ng-repeat="item in row | extractItems track by item.id"

View File

@ -34,6 +34,7 @@ module.exports = function (config) {
],
files: [
'bower_components/jquery/dist/jquery.min.js',
'bower_components/angular/angular.js',
'bower_components/angular-mocks/angular-mocks.js',
'merlin/static/merlin/js/libs/underscore/underscore-min.js',

View File

@ -16,29 +16,72 @@
.directive('editable', function() {
return {
restrict: 'E',
templateUrl: '/static/merlin/templates/editable-popup.html',
scope: {
label: '@',
value: '='
},
link: function(scope, element) {
angular.element(element).find('a[data-toggle="popover"]')
.popover({html: true})
.on('click', function(e) {
e.preventDefault();
return true;
});
templateUrl: '/static/merlin/templates/editable.html',
require: 'ngModel',
scope: true,
link: function(scope, element, attrs, ngModelCtrl) {
var hiddenSpan = element.find('span.width-detector'),
input = element.find('input'),
maxWidth = 400;
function adjustWidth() {
var width;
hiddenSpan.html(scope.editableValue);
width = hiddenSpan.width();
input.width(width <= maxWidth ? width : maxWidth);
}
function accept() {
ngModelCtrl.$setViewValue(scope.editableValue);
scope.isEdited = false;
}
function reject() {
ngModelCtrl.$rollbackViewValue();
scope.isEdited = false;
}
scope.isEdited = false;
scope.$watch('editableValue', function() {
adjustWidth();
});
input.on('keyup', function(e) {
if ( e.keyCode == 13 ) {
accept();
scope.$apply();
} else if (e.keyCode == 27 ) {
reject();
scope.$apply();
}
});
ngModelCtrl.$render = function() {
if ( !ngModelCtrl.$viewValue ) {
ngModelCtrl.$viewValue = ngModelCtrl.$modelValue;
}
scope.editableValue = ngModelCtrl.$viewValue;
adjustWidth();
};
scope.accept = accept;
scope.reject = reject;
}
};
})
.directive('showFocus', function($timeout) {
return function(scope, element, attrs) {
scope.$watch(attrs.showFocus, function(newValue) {
$timeout(function() {
newValue && element.focus();
});
});
}
})
.directive('panel', function($parse) {
return {
restrict: 'E',
templateUrl: '/static/merlin/templates/collapsible-panel.html',
transclude: true,
scope: {
title: '@',
onRemove: '&'
panel: '=content'
},
link: function(scope, element, attrs) {
scope.removable = $parse(attrs.removable)();

View File

@ -188,6 +188,13 @@
if ( !Object.keys(_items).length ) {
self.getIDs().forEach(function(id) {
_items[id] = self.getByID(id);
_items[id].keyValue = function() {
if ( !arguments.length ) {
return this.getID();
} else {
this.setID(arguments[0]);
}
};
});
}
return _items;

View File

@ -36,9 +36,15 @@
}
return this;
},
getTitle: function() {
title: function() {
var entity;
if ( this._barricadeContainer ) {
return this._barricadeContainer.getByID(this._barricadeId).get('name');
entity = this._barricadeContainer.getByID(this._barricadeId).get('name');
if ( arguments.length ) {
entity.set(arguments[0]);
} else {
return entity.get();
}
}
},
remove: function() {

View File

@ -54,6 +54,10 @@
}
}
editable .fa-pencil {
color: black;
}
.section {
.form-group {
padding-left: 15px;

View File

@ -1,11 +1,13 @@
<div class="panel panel-default merlin-panel">
<div class="panel-heading" ng-show="title">
<div class="panel-heading" ng-show="panel.title()">
<h4 class="panel-title">
<a ng-click="isCollapsed = !isCollapsed" href="#">
<i class="fa fa-lg" ng-class="isCollapsed ? 'fa-caret-right' : 'fa-caret-down'"></i></a>
{$ title $}
<a href="#" ng-click="onRemove()"><i ng-show="removable" class="fa fa-times-circle pull-right"></i></a></h4>
<editable ng-model="panel.title" ng-model-options="{getterSetter: true}"></editable>
<a href="#" ng-show="panel.removable" ng-click="panel.remove()">
<i class="fa fa-times-circle pull-right"></i></a>
</h4>
</div>
<div collapse="isCollapsed" class="panel-body" ng-transclude>
</div>
</div>
</div>

View File

@ -1,7 +0,0 @@
{$ value $}
<a href="#" data-toggle="popover" data-trigger="click" data-container="body"
data-content="<label>{$ label $}</label>
<input type='text' class='form-control' value='{$ value $}'>
<button class='btn btn-default btn-sm cancel'>Cancel</button>
<button class='btn btn-primary btn-sm'>Save</button>">
<i class="fa fa-pencil"></i></a>

View File

@ -0,0 +1,10 @@
<span class="width-detector" style="display:none"></span>
<span ng-show="!isEdited">
<span ng-bind="editableValue"></span>
<a ng-click="isEdited = true" href="#"><i class="fa fa-pencil"></i></a>
</span>
<span ng-show="isEdited">
<input type="text" ng-model="editableValue" show-focus="isEdited">
<button ng-click="accept()"><i class="fa fa-check"></i></button>
<button ng-click="reject()"><i class="fa fa-close"></i></button>
</span>

View File

@ -3,7 +3,7 @@
<div class="left-column">
<div class="form-group">
<label for="elem-{$ $id $}.{$ key $}">
<editable value="key" label="New Name"></editable>
<editable ng-model="subvalue.keyValue" ng-model-options="{getterSetter: true}"></editable>
</label>
<div class="input-group">
<input id="elem-{$ $id $}.{$ key $}" type="text" class="form-control" ng-model="subvalue.value"

View File

@ -42,12 +42,11 @@ describe('merlin directives', function() {
}
function getPanelRemoveButton(panelElem) {
var iTag = panelElem.find('i').eq(1);
return iTag.hasClass('fa-times-circle') && iTag;
return panelElem.find('a').eq(2);
}
function getCollapseBtn(groupElem) {
return groupElem.find('a').eq(0);
function getCollapseBtn(panelElem) {
return panelElem.find('a').eq(0);
}
function getPanelBody(panelElem) {
@ -55,8 +54,8 @@ describe('merlin directives', function() {
return div.hasClass('panel-body') && div;
}
function makePanelElem(contents) {
var panel = $compile('<panel ' + contents + '></panel>')($scope);
function makePanelElem(content) {
var panel = $compile('<panel content="' + content + '"></panel>')($scope);
$scope.$digest();
return panel;
}
@ -67,35 +66,35 @@ describe('merlin directives', function() {
return element;
}
it('shows panel heading when and only when title is passed via attr', function() {
it('shows panel heading when and only when its title() is not false', function() {
var title = 'My Panel',
element1 = makePanelElem('title="' + title +'"'),
element2 = makePanelElem('');
element1, element2;
$scope.panel1 = {
title: function() { return title; }
};
$scope.panel2 = {};
element1 = makePanelElem('panel1');
element2 = makePanelElem('');
expect(getPanelHeading(element1).hasClass('ng-hide')).toBe(false);
expect(element1.html()).toContain(title);
expect(getPanelHeading(element2).hasClass('ng-hide')).toBe(true);
});
it('requires both `title` and `removable` to be removable', function() {
it('requires both `.title()` and `.removable` to be removable', function() {
var title = 'My Panel',
element1, element2;
element1 = makePanelElem('title="' + title +'" removable="true"');
element2 = makePanelElem('title="' + title +'"');
expect(getPanelRemoveButton(element1).hasClass('ng-hide')).toBe(false);
expect(getPanelRemoveButton(element2).hasClass('ng-hide')).toBe(true);
});
it('with `on-remove`, but without `removable` is not removable', function() {
var title = 'My Panel',
element1, element2;
$scope.remove = function() {};
element1 = makePanelElem(
'title="' + title +'" removable="true" on-remove="remove()"');
element2 = makePanelElem('title="' + title +'" on-remove="remove()"');
$scope.panel1 = {
title: function() { return title; },
removable: true
};
$scope.panel2 = {
title: function() { return title; }
};
element1 = makePanelElem('panel1');
element2 = makePanelElem('panel2');
expect(getPanelRemoveButton(element1).hasClass('ng-hide')).toBe(false);
expect(getPanelRemoveButton(element2).hasClass('ng-hide')).toBe(true);
@ -253,4 +252,67 @@ describe('merlin directives', function() {
expect(element.html()).toContain('<textarea');
})
});
xdescribe("'show-focus'", function() {
var element;
beforeEach(function() {
element = $compile(
'<div><input type="text" ng-show="show" show-focus="show"></div>')($scope);
$scope.$digest();
});
it('allows to immediately set focus on element after it was shown', function() {
expect(element.is(':focus')).toBe(false);
$scope.show = true;
$scope.$apply();
expect(element.is(':focus')).toBe(true);
})
});
describe('<editable>', function() {
it('starts with the value not being edited', function() {
});
it("enters the editing mode once user clicks 'fa-pencil' icon", function() {
});
describe('during editing', function() {
it("pressing any key except 'Enter' or 'Esc' neither exits editing state, nor changes model", function() {
});
it("pressing 'Enter' key changes model and exits editing state", function() {
});
it("clicking 'fa-check' icon changes model and exits editing state", function() {
});
it("pressing 'Esc' key exits editing state without changing model", function() {
});
it("clicking 'fa-close' icon exits editing state without changing model", function() {
});
describe('edit box automatically enlarges', function() {
it('to fit the value being edited', function() {
});
it('up to the limit', function() {
});
})
});
});
});