Merge branch 'master' into open671
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
<div class="abs t-about l-about t-about-openmctweb s-about" ng-controller = "AboutController as about">
|
||||
<div class="l-splash s-splash"></div>
|
||||
<div class="s-text l-content">
|
||||
<h1 class="l-title s-title">OpenMCT Web</h1>
|
||||
<h1 class="l-title s-title">Open MCT</h1>
|
||||
<div class="l-description s-description">
|
||||
<p>Open MCT Web, Copyright © 2014-2015, United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All rights reserved.</p>
|
||||
<p>Open MCT Web is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at <a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">http://www.apache.org/licenses/LICENSE-2.0</a>.</p>
|
||||
|
||||
@@ -31,11 +31,13 @@ define([
|
||||
"./src/actions/PropertiesAction",
|
||||
"./src/actions/RemoveAction",
|
||||
"./src/actions/SaveAction",
|
||||
"./src/actions/SaveAsAction",
|
||||
"./src/actions/CancelAction",
|
||||
"./src/policies/EditActionPolicy",
|
||||
"./src/policies/EditableLinkPolicy",
|
||||
"./src/policies/EditableMovePolicy",
|
||||
"./src/policies/EditNavigationPolicy",
|
||||
"./src/policies/EditContextualActionPolicy",
|
||||
"./src/representers/EditRepresenter",
|
||||
"./src/representers/EditToolbarRepresenter",
|
||||
"text!./res/templates/library.html",
|
||||
@@ -55,11 +57,13 @@ define([
|
||||
PropertiesAction,
|
||||
RemoveAction,
|
||||
SaveAction,
|
||||
SaveAsAction,
|
||||
CancelAction,
|
||||
EditActionPolicy,
|
||||
EditableLinkPolicy,
|
||||
EditableMovePolicy,
|
||||
EditNavigationPolicy,
|
||||
EditContextualActionPolicy,
|
||||
EditRepresenter,
|
||||
EditToolbarRepresenter,
|
||||
libraryTemplate,
|
||||
@@ -163,6 +167,15 @@ define([
|
||||
"implementation": SaveAction,
|
||||
"name": "Save",
|
||||
"description": "Save changes made to these objects.",
|
||||
"depends": [],
|
||||
"priority": "mandatory"
|
||||
},
|
||||
{
|
||||
"key": "save",
|
||||
"category": "conclude-editing",
|
||||
"implementation": SaveAsAction,
|
||||
"name": "Save",
|
||||
"description": "Save changes made to these objects.",
|
||||
"depends": [
|
||||
"$injector",
|
||||
"policyService",
|
||||
@@ -189,6 +202,11 @@ define([
|
||||
"category": "action",
|
||||
"implementation": EditActionPolicy
|
||||
},
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": EditContextualActionPolicy,
|
||||
"depends": ["navigationService", "editModeBlacklist", "nonEditContextBlacklist"]
|
||||
},
|
||||
{
|
||||
"category": "action",
|
||||
"implementation": EditableMovePolicy
|
||||
@@ -254,6 +272,16 @@ define([
|
||||
{
|
||||
"implementation": EditToolbarRepresenter
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
{
|
||||
"key":"editModeBlacklist",
|
||||
"value": ["copy", "follow", "window", "link", "locate"]
|
||||
},
|
||||
{
|
||||
"key": "nonEditContextBlacklist",
|
||||
"value": ["copy", "follow", "properties", "move", "link", "remove", "locate"]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
['../../../browse/src/creation/CreateWizard'],
|
||||
function (CreateWizard) {
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* The "Save" action; the action triggered by clicking Save from
|
||||
@@ -33,31 +33,11 @@ define(
|
||||
* @memberof platform/commonUI/edit
|
||||
*/
|
||||
function SaveAction(
|
||||
$injector,
|
||||
policyService,
|
||||
dialogService,
|
||||
creationService,
|
||||
copyService,
|
||||
context
|
||||
) {
|
||||
this.domainObject = (context || {}).domainObject;
|
||||
this.injectObjectService = function(){
|
||||
this.objectService = $injector.get("objectService");
|
||||
};
|
||||
this.policyService = policyService;
|
||||
this.dialogService = dialogService;
|
||||
this.creationService = creationService;
|
||||
this.copyService = copyService;
|
||||
}
|
||||
|
||||
SaveAction.prototype.getObjectService = function(){
|
||||
// Lazily acquire object service (avoids cyclical dependency)
|
||||
if (!this.objectService) {
|
||||
this.injectObjectService();
|
||||
}
|
||||
return this.objectService;
|
||||
};
|
||||
|
||||
/**
|
||||
* Save changes and conclude editing.
|
||||
*
|
||||
@@ -66,9 +46,7 @@ define(
|
||||
* @memberof platform/commonUI/edit.SaveAction#
|
||||
*/
|
||||
SaveAction.prototype.perform = function () {
|
||||
var domainObject = this.domainObject,
|
||||
copyService = this.copyService,
|
||||
self = this;
|
||||
var domainObject = this.domainObject;
|
||||
|
||||
function resolveWith(object){
|
||||
return function () {
|
||||
@@ -76,63 +54,13 @@ define(
|
||||
};
|
||||
}
|
||||
|
||||
function doWizardSave(parent) {
|
||||
var wizard = new CreateWizard(
|
||||
domainObject,
|
||||
parent,
|
||||
self.policyService
|
||||
);
|
||||
|
||||
return self.dialogService
|
||||
.getUserInput(
|
||||
wizard.getFormStructure(true),
|
||||
wizard.getInitialFormValue()
|
||||
)
|
||||
.then(wizard.populateObjectFromInput.bind(wizard));
|
||||
}
|
||||
|
||||
function fetchObject(objectId){
|
||||
return self.getObjectService().getObjects([objectId]).then(function(objects){
|
||||
return objects[objectId];
|
||||
});
|
||||
}
|
||||
|
||||
function getParent(object){
|
||||
return fetchObject(object.getModel().location);
|
||||
}
|
||||
|
||||
function allowClone(objectToClone) {
|
||||
return (objectToClone.getId() === domainObject.getId()) ||
|
||||
objectToClone.getCapability('location').isOriginal();
|
||||
}
|
||||
|
||||
function cloneIntoParent(parent) {
|
||||
return copyService.perform(domainObject, parent, allowClone);
|
||||
}
|
||||
|
||||
function cancelEditingAfterClone(clonedObject) {
|
||||
return domainObject.getCapability("editor").cancel()
|
||||
.then(resolveWith(clonedObject));
|
||||
}
|
||||
|
||||
// Invoke any save behavior introduced by the editor capability;
|
||||
// this is introduced by EditableDomainObject which is
|
||||
// used to insulate underlying objects from changes made
|
||||
// during editing.
|
||||
function doSave() {
|
||||
//This is a new 'virtual object' that has not been persisted
|
||||
// yet.
|
||||
if (domainObject.getModel().persisted === undefined){
|
||||
return getParent(domainObject)
|
||||
.then(doWizardSave)
|
||||
.then(getParent)
|
||||
.then(cloneIntoParent)
|
||||
.then(cancelEditingAfterClone)
|
||||
.catch(resolveWith(false));
|
||||
} else {
|
||||
return domainObject.getCapability("editor").save()
|
||||
.then(resolveWith(domainObject.getOriginalObject()));
|
||||
}
|
||||
return domainObject.getCapability("editor").save()
|
||||
.then(resolveWith(domainObject.getOriginalObject()));
|
||||
}
|
||||
|
||||
// Discard the current root view (which will be the editing
|
||||
@@ -157,7 +85,8 @@ define(
|
||||
SaveAction.appliesTo = function (context) {
|
||||
var domainObject = (context || {}).domainObject;
|
||||
return domainObject !== undefined &&
|
||||
domainObject.hasCapability("editor");
|
||||
domainObject.hasCapability("editor") &&
|
||||
domainObject.getModel().persisted !== undefined;
|
||||
};
|
||||
|
||||
return SaveAction;
|
||||
|
||||
166
platform/commonUI/edit/src/actions/SaveAsAction.js
Normal file
166
platform/commonUI/edit/src/actions/SaveAsAction.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
define(
|
||||
['../../../browse/src/creation/CreateWizard'],
|
||||
function (CreateWizard) {
|
||||
|
||||
/**
|
||||
* The "Save" action; the action triggered by clicking Save from
|
||||
* Edit Mode. Exits the editing user interface and invokes object
|
||||
* capabilities to persist the changes that have been made.
|
||||
* @constructor
|
||||
* @implements {Action}
|
||||
* @memberof platform/commonUI/edit
|
||||
*/
|
||||
function SaveAsAction(
|
||||
$injector,
|
||||
policyService,
|
||||
dialogService,
|
||||
creationService,
|
||||
copyService,
|
||||
context
|
||||
) {
|
||||
this.domainObject = (context || {}).domainObject;
|
||||
this.injectObjectService = function(){
|
||||
this.objectService = $injector.get("objectService");
|
||||
};
|
||||
this.policyService = policyService;
|
||||
this.dialogService = dialogService;
|
||||
this.creationService = creationService;
|
||||
this.copyService = copyService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
SaveAsAction.prototype.createWizard = function (parent) {
|
||||
return new CreateWizard(
|
||||
this.domainObject,
|
||||
parent,
|
||||
this.policyService
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
SaveAsAction.prototype.getObjectService = function(){
|
||||
// Lazily acquire object service (avoids cyclical dependency)
|
||||
if (!this.objectService) {
|
||||
this.injectObjectService();
|
||||
}
|
||||
return this.objectService;
|
||||
};
|
||||
|
||||
function resolveWith(object){
|
||||
return function () {
|
||||
return object;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save changes and conclude editing.
|
||||
*
|
||||
* @returns {Promise} a promise that will be fulfilled when
|
||||
* cancellation has completed
|
||||
* @memberof platform/commonUI/edit.SaveAction#
|
||||
*/
|
||||
SaveAsAction.prototype.perform = function () {
|
||||
// Discard the current root view (which will be the editing
|
||||
// UI, which will have been pushed atop the Browse UI.)
|
||||
function returnToBrowse(object) {
|
||||
if (object) {
|
||||
object.getCapability("action").perform("navigate");
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
return this.save().then(returnToBrowse);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
SaveAsAction.prototype.save = function () {
|
||||
var self = this,
|
||||
domainObject = this.domainObject,
|
||||
copyService = this.copyService;
|
||||
|
||||
function doWizardSave(parent) {
|
||||
var wizard = self.createWizard(parent);
|
||||
|
||||
return self.dialogService
|
||||
.getUserInput(wizard.getFormStructure(true),
|
||||
wizard.getInitialFormValue()
|
||||
).then(wizard.populateObjectFromInput.bind(wizard));
|
||||
}
|
||||
|
||||
function fetchObject(objectId){
|
||||
return self.getObjectService().getObjects([objectId]).then(function(objects){
|
||||
return objects[objectId];
|
||||
});
|
||||
}
|
||||
|
||||
function getParent(object){
|
||||
return fetchObject(object.getModel().location);
|
||||
}
|
||||
|
||||
function allowClone(objectToClone) {
|
||||
return (objectToClone.getId() === domainObject.getId()) ||
|
||||
objectToClone.getCapability('location').isOriginal();
|
||||
}
|
||||
|
||||
function cloneIntoParent(parent) {
|
||||
return copyService.perform(domainObject, parent, allowClone);
|
||||
}
|
||||
|
||||
function cancelEditingAfterClone(clonedObject) {
|
||||
return domainObject.getCapability("editor").cancel()
|
||||
.then(resolveWith(clonedObject));
|
||||
}
|
||||
|
||||
return getParent(domainObject)
|
||||
.then(doWizardSave)
|
||||
.then(getParent)
|
||||
.then(cloneIntoParent)
|
||||
.then(cancelEditingAfterClone)
|
||||
.catch(resolveWith(false));
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if this action is applicable in a given context.
|
||||
* This will ensure that a domain object is present in the context,
|
||||
* and that this domain object is in Edit mode.
|
||||
* @returns true if applicable
|
||||
*/
|
||||
SaveAsAction.appliesTo = function (context) {
|
||||
var domainObject = (context || {}).domainObject;
|
||||
return domainObject !== undefined &&
|
||||
domainObject.hasCapability("editor") &&
|
||||
domainObject.getModel().persisted === undefined;
|
||||
};
|
||||
|
||||
return SaveAsAction;
|
||||
}
|
||||
);
|
||||
@@ -36,14 +36,6 @@ define(
|
||||
this.policyService = policyService;
|
||||
}
|
||||
|
||||
function applicableView(key){
|
||||
return ['plot', 'scrolling'].indexOf(key) >= 0;
|
||||
}
|
||||
|
||||
function editableType(key){
|
||||
return key === 'telemetry.panel';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a count of views which are not flagged as non-editable.
|
||||
* @private
|
||||
@@ -63,7 +55,8 @@ define(
|
||||
|
||||
// A view is editable unless explicitly flagged as not
|
||||
(views || []).forEach(function (view) {
|
||||
if (view.editable === true || (applicableView(view.key) && editableType(type.getKey()))) {
|
||||
if (view.editable === true ||
|
||||
(view.key === 'plot' && type.getKey() === 'telemetry.panel')) {
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* Policy controlling whether the context menu is visible when
|
||||
* objects are being edited
|
||||
* @param navigationService
|
||||
* @param editModeBlacklist A blacklist of actions disallowed from
|
||||
* context menu when navigated object is being edited
|
||||
* @param nonEditContextBlacklist A blacklist of actions disallowed
|
||||
* from context menu of non-editable objects, when navigated object
|
||||
* is being edited
|
||||
* @constructor
|
||||
*/
|
||||
function EditContextualActionPolicy(navigationService, editModeBlacklist, nonEditContextBlacklist) {
|
||||
this.navigationService = navigationService;
|
||||
|
||||
//The list of objects disallowed on target object when in edit mode
|
||||
this.editModeBlacklist = editModeBlacklist;
|
||||
//The list of objects disallowed on target object that is not in
|
||||
// edit mode (ie. the context menu in the tree on the LHS).
|
||||
this.nonEditContextBlacklist = nonEditContextBlacklist;
|
||||
}
|
||||
|
||||
function isParentEditable(object) {
|
||||
var parent = object.hasCapability("context") && object.getCapability("context").getParent();
|
||||
return !!parent && parent.hasCapability("editor");
|
||||
}
|
||||
|
||||
EditContextualActionPolicy.prototype.allow = function (action, context) {
|
||||
var selectedObject = context.domainObject,
|
||||
navigatedObject = this.navigationService.getNavigation(),
|
||||
actionMetadata = action.getMetadata ? action.getMetadata() : {};
|
||||
|
||||
if (navigatedObject.hasCapability('editor')) {
|
||||
if (selectedObject.hasCapability('editor') || isParentEditable(selectedObject)){
|
||||
return this.editModeBlacklist.indexOf(actionMetadata.key) === -1;
|
||||
} else {
|
||||
//Target is in the context menu
|
||||
return this.nonEditContextBlacklist.indexOf(actionMetadata.key) === -1;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return EditContextualActionPolicy;
|
||||
}
|
||||
);
|
||||
@@ -25,11 +25,11 @@ define(
|
||||
function (SaveAction) {
|
||||
|
||||
describe("The Save action", function () {
|
||||
var mockLocation,
|
||||
mockDomainObject,
|
||||
var mockDomainObject,
|
||||
mockEditorCapability,
|
||||
mockUrlService,
|
||||
actionContext,
|
||||
mockActionCapability,
|
||||
capabilities = {},
|
||||
action;
|
||||
|
||||
function mockPromise(value) {
|
||||
@@ -41,65 +41,62 @@ define(
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockLocation = jasmine.createSpyObj(
|
||||
"$location",
|
||||
[ "path" ]
|
||||
);
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[ "getCapability", "hasCapability" ]
|
||||
[
|
||||
"getCapability",
|
||||
"hasCapability",
|
||||
"getModel",
|
||||
"getOriginalObject"
|
||||
]
|
||||
);
|
||||
mockEditorCapability = jasmine.createSpyObj(
|
||||
"editor",
|
||||
[ "save", "cancel" ]
|
||||
);
|
||||
mockUrlService = jasmine.createSpyObj(
|
||||
"urlService",
|
||||
["urlForLocation"]
|
||||
mockActionCapability = jasmine.createSpyObj(
|
||||
"actionCapability",
|
||||
[ "perform"]
|
||||
);
|
||||
|
||||
capabilities.editor = mockEditorCapability;
|
||||
capabilities.action = mockActionCapability;
|
||||
|
||||
actionContext = {
|
||||
domainObject: mockDomainObject
|
||||
};
|
||||
|
||||
mockDomainObject.hasCapability.andReturn(true);
|
||||
mockDomainObject.getCapability.andReturn(mockEditorCapability);
|
||||
mockDomainObject.getCapability.andCallFake(function (capability) {
|
||||
return capabilities[capability];
|
||||
});
|
||||
mockDomainObject.getModel.andReturn({persisted: 0});
|
||||
mockEditorCapability.save.andReturn(mockPromise(true));
|
||||
mockDomainObject.getOriginalObject.andReturn(mockDomainObject);
|
||||
|
||||
action = new SaveAction(mockLocation, mockUrlService, actionContext);
|
||||
action = new SaveAction(actionContext);
|
||||
|
||||
});
|
||||
|
||||
it("only applies to domain object with an editor capability", function () {
|
||||
expect(SaveAction.appliesTo(actionContext)).toBeTruthy();
|
||||
expect(SaveAction.appliesTo(actionContext)).toBe(true);
|
||||
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
|
||||
|
||||
mockDomainObject.hasCapability.andReturn(false);
|
||||
mockDomainObject.getCapability.andReturn(undefined);
|
||||
expect(SaveAction.appliesTo(actionContext)).toBeFalsy();
|
||||
expect(SaveAction.appliesTo(actionContext)).toBe(false);
|
||||
});
|
||||
|
||||
//TODO: Disabled for NEM Beta
|
||||
xit("invokes the editor capability's save functionality when performed", function () {
|
||||
// Verify precondition
|
||||
expect(mockEditorCapability.save).not.toHaveBeenCalled();
|
||||
action.perform();
|
||||
|
||||
// Should have called cancel
|
||||
expect(mockEditorCapability.save).toHaveBeenCalled();
|
||||
|
||||
// Also shouldn't call cancel
|
||||
expect(mockEditorCapability.cancel).not.toHaveBeenCalled();
|
||||
it("only applies to domain object that has already been persisted",
|
||||
function () {
|
||||
mockDomainObject.getModel.andReturn({persisted: undefined});
|
||||
expect(SaveAction.appliesTo(actionContext)).toBe(false);
|
||||
});
|
||||
|
||||
//TODO: Disabled for NEM Beta
|
||||
xit("returns to browse when performed", function () {
|
||||
action.perform();
|
||||
expect(mockLocation.path).toHaveBeenCalledWith(
|
||||
mockUrlService.urlForLocation("browse", mockDomainObject)
|
||||
);
|
||||
});
|
||||
it("uses the editor capability to save the object",
|
||||
function () {
|
||||
action.perform();
|
||||
expect(mockEditorCapability.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
173
platform/commonUI/edit/test/actions/SaveAsActionSpec.js
Normal file
173
platform/commonUI/edit/test/actions/SaveAsActionSpec.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/actions/SaveAsAction"],
|
||||
function (SaveAsAction) {
|
||||
|
||||
describe("The Save As action", function () {
|
||||
var mockDomainObject,
|
||||
mockEditorCapability,
|
||||
mockActionCapability,
|
||||
mockObjectService,
|
||||
mockDialogService,
|
||||
mockCopyService,
|
||||
mockParent,
|
||||
mockUrlService,
|
||||
actionContext,
|
||||
capabilities = {},
|
||||
action;
|
||||
|
||||
function noop () {}
|
||||
|
||||
function mockPromise(value) {
|
||||
return (value || {}).then ? value :
|
||||
{
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
},
|
||||
catch: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
} ;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
"domainObject",
|
||||
[
|
||||
"getCapability",
|
||||
"hasCapability",
|
||||
"getModel"
|
||||
]
|
||||
);
|
||||
mockDomainObject.hasCapability.andReturn(true);
|
||||
mockDomainObject.getCapability.andCallFake(function (capability) {
|
||||
return capabilities[capability];
|
||||
});
|
||||
mockDomainObject.getModel.andReturn({location: 'a', persisted: undefined});
|
||||
|
||||
mockParent = jasmine.createSpyObj(
|
||||
"parentObject",
|
||||
[
|
||||
"getCapability",
|
||||
"hasCapability",
|
||||
"getModel"
|
||||
]
|
||||
);
|
||||
|
||||
mockEditorCapability = jasmine.createSpyObj(
|
||||
"editor",
|
||||
[ "save", "cancel" ]
|
||||
);
|
||||
mockEditorCapability.cancel.andReturn(mockPromise(undefined));
|
||||
mockEditorCapability.save.andReturn(mockPromise(true));
|
||||
capabilities.editor = mockEditorCapability;
|
||||
|
||||
mockActionCapability = jasmine.createSpyObj(
|
||||
"action",
|
||||
["perform"]
|
||||
);
|
||||
capabilities.action = mockActionCapability;
|
||||
|
||||
mockObjectService = jasmine.createSpyObj(
|
||||
"objectService",
|
||||
["getObjects"]
|
||||
);
|
||||
mockObjectService.getObjects.andReturn(mockPromise({'a': mockParent}));
|
||||
|
||||
mockDialogService = jasmine.createSpyObj(
|
||||
"dialogService",
|
||||
[
|
||||
"getUserInput"
|
||||
]
|
||||
);
|
||||
mockDialogService.getUserInput.andReturn(mockPromise(undefined));
|
||||
|
||||
mockCopyService = jasmine.createSpyObj(
|
||||
"copyService",
|
||||
[
|
||||
"perform"
|
||||
]
|
||||
);
|
||||
|
||||
mockUrlService = jasmine.createSpyObj(
|
||||
"urlService",
|
||||
["urlForLocation"]
|
||||
);
|
||||
|
||||
actionContext = {
|
||||
domainObject: mockDomainObject
|
||||
};
|
||||
|
||||
action = new SaveAsAction(undefined, undefined, mockDialogService, undefined, mockCopyService, actionContext);
|
||||
|
||||
spyOn(action, "getObjectService");
|
||||
action.getObjectService.andReturn(mockObjectService);
|
||||
|
||||
spyOn(action, "createWizard");
|
||||
action.createWizard.andReturn({
|
||||
getFormStructure: noop,
|
||||
getInitialFormValue: noop,
|
||||
populateObjectFromInput: function() {
|
||||
return mockDomainObject;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("only applies to domain object with an editor capability", function () {
|
||||
expect(SaveAsAction.appliesTo(actionContext)).toBe(true);
|
||||
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
|
||||
|
||||
mockDomainObject.hasCapability.andReturn(false);
|
||||
mockDomainObject.getCapability.andReturn(undefined);
|
||||
expect(SaveAsAction.appliesTo(actionContext)).toBe(false);
|
||||
});
|
||||
|
||||
it("only applies to domain object that has not already been" +
|
||||
" persisted", function () {
|
||||
expect(SaveAsAction.appliesTo(actionContext)).toBe(true);
|
||||
expect(mockDomainObject.hasCapability).toHaveBeenCalledWith("editor");
|
||||
|
||||
mockDomainObject.getModel.andReturn({persisted: 0});
|
||||
expect(SaveAsAction.appliesTo(actionContext)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns to browse after save", function () {
|
||||
spyOn(action, "save");
|
||||
action.save.andReturn(mockPromise(mockDomainObject));
|
||||
action.perform();
|
||||
expect(mockActionCapability.perform).toHaveBeenCalledWith(
|
||||
"navigate"
|
||||
);
|
||||
});
|
||||
|
||||
it("prompts the user for object details", function () {
|
||||
action.perform();
|
||||
expect(mockDialogService.getUserInput).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -33,19 +33,43 @@ define(
|
||||
mockDomainObject,
|
||||
mockEditAction,
|
||||
mockPropertiesAction,
|
||||
mockTypeCapability,
|
||||
mockStatusCapability,
|
||||
capabilities,
|
||||
plotView,
|
||||
policy;
|
||||
|
||||
beforeEach(function () {
|
||||
mockDomainObject = jasmine.createSpyObj(
|
||||
'domainObject',
|
||||
[ 'useCapability' ]
|
||||
[
|
||||
'useCapability',
|
||||
'hasCapability',
|
||||
'getCapability'
|
||||
]
|
||||
);
|
||||
mockStatusCapability = jasmine.createSpyObj('statusCapability', ['get']);
|
||||
mockStatusCapability.get.andReturn(false);
|
||||
mockTypeCapability = jasmine.createSpyObj('type', ['getKey']);
|
||||
capabilities = {
|
||||
'status': mockStatusCapability,
|
||||
'type': mockTypeCapability
|
||||
};
|
||||
|
||||
mockEditAction = jasmine.createSpyObj('edit', ['getMetadata']);
|
||||
mockPropertiesAction = jasmine.createSpyObj('edit', ['getMetadata']);
|
||||
|
||||
mockDomainObject.getCapability.andCallFake(function(capability){
|
||||
return capabilities[capability];
|
||||
});
|
||||
mockDomainObject.hasCapability.andCallFake(function(capability){
|
||||
return !!capabilities[capability];
|
||||
});
|
||||
|
||||
editableView = { editable: true };
|
||||
nonEditableView = { editable: false };
|
||||
undefinedView = { someKey: "some value" };
|
||||
plotView = { key: "plot", editable: false };
|
||||
testViews = [];
|
||||
|
||||
mockDomainObject.useCapability.andCallFake(function (c) {
|
||||
@@ -64,38 +88,53 @@ define(
|
||||
policy = new EditActionPolicy();
|
||||
});
|
||||
|
||||
//TODO: Disabled for NEM Beta
|
||||
xit("allows the edit action when there are editable views", function () {
|
||||
it("allows the edit action when there are editable views", function () {
|
||||
testViews = [ editableView ];
|
||||
expect(policy.allow(mockEditAction, testContext)).toBeTruthy();
|
||||
// No edit flag defined; should be treated as editable
|
||||
testViews = [ undefinedView, undefinedView ];
|
||||
expect(policy.allow(mockEditAction, testContext)).toBeTruthy();
|
||||
expect(policy.allow(mockEditAction, testContext)).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Disabled for NEM Beta
|
||||
xit("allows the edit properties action when there are no editable views", function () {
|
||||
it("allows the edit properties action when there are no editable views", function () {
|
||||
testViews = [ nonEditableView, nonEditableView ];
|
||||
expect(policy.allow(mockPropertiesAction, testContext)).toBeTruthy();
|
||||
expect(policy.allow(mockPropertiesAction, testContext)).toBe(true);
|
||||
});
|
||||
|
||||
//TODO: Disabled for NEM Beta
|
||||
xit("disallows the edit action when there are no editable views", function () {
|
||||
it("disallows the edit action when there are no editable views", function () {
|
||||
testViews = [ nonEditableView, nonEditableView ];
|
||||
expect(policy.allow(mockEditAction, testContext)).toBeFalsy();
|
||||
expect(policy.allow(mockEditAction, testContext)).toBe(false);
|
||||
});
|
||||
|
||||
//TODO: Disabled for NEM Beta
|
||||
xit("disallows the edit properties action when there are" +
|
||||
it("disallows the edit properties action when there are" +
|
||||
" editable views", function () {
|
||||
testViews = [ editableView ];
|
||||
expect(policy.allow(mockPropertiesAction, testContext)).toBeFalsy();
|
||||
expect(policy.allow(mockPropertiesAction, testContext)).toBe(false);
|
||||
});
|
||||
|
||||
it("disallows the edit action when object is already being" +
|
||||
" edited", function () {
|
||||
testViews = [ editableView ];
|
||||
mockStatusCapability.get.andReturn(true);
|
||||
expect(policy.allow(mockEditAction, testContext)).toBe(false);
|
||||
});
|
||||
|
||||
it("allows editing of panels in plot view", function () {
|
||||
testViews = [ plotView ];
|
||||
mockTypeCapability.getKey.andReturn('telemetry.panel');
|
||||
|
||||
expect(policy.allow(mockEditAction, testContext)).toBe(true);
|
||||
});
|
||||
|
||||
it("disallows editing of plot view when object not a panel type", function () {
|
||||
testViews = [ plotView ];
|
||||
mockTypeCapability.getKey.andReturn('something.else');
|
||||
|
||||
expect(policy.allow(mockEditAction, testContext)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
it("allows the edit properties outside of the 'view-control' category", function () {
|
||||
testViews = [ nonEditableView ];
|
||||
testContext.category = "something-else";
|
||||
expect(policy.allow(mockPropertiesAction, testContext)).toBeTruthy();
|
||||
expect(policy.allow(mockPropertiesAction, testContext)).toBe(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global describe,it,expect,beforeEach,jasmine*/
|
||||
|
||||
define(
|
||||
["../../src/policies/EditContextualActionPolicy"],
|
||||
function (EditContextualActionPolicy) {
|
||||
|
||||
describe("The Edit contextual action policy", function () {
|
||||
var policy,
|
||||
navigationService,
|
||||
mockAction,
|
||||
context,
|
||||
navigatedObject,
|
||||
mockDomainObject,
|
||||
metadata,
|
||||
editModeBlacklist = ["copy", "follow", "window", "link", "locate"],
|
||||
nonEditContextBlacklist = ["copy", "follow", "properties", "move", "link", "remove", "locate"];
|
||||
|
||||
beforeEach(function () {
|
||||
navigatedObject = jasmine.createSpyObj("navigatedObject", ["hasCapability"]);
|
||||
navigatedObject.hasCapability.andReturn(false);
|
||||
|
||||
mockDomainObject = jasmine.createSpyObj("domainObject", ["hasCapability", "getCapability"]);
|
||||
mockDomainObject.hasCapability.andReturn(false);
|
||||
|
||||
navigationService = jasmine.createSpyObj("navigationService", ["getNavigation"]);
|
||||
navigationService.getNavigation.andReturn(navigatedObject);
|
||||
|
||||
metadata = {key: "move"};
|
||||
mockAction = jasmine.createSpyObj("action", ["getMetadata"]);
|
||||
mockAction.getMetadata.andReturn(metadata);
|
||||
|
||||
context = {domainObject: mockDomainObject};
|
||||
|
||||
policy = new EditContextualActionPolicy(navigationService, editModeBlacklist, nonEditContextBlacklist);
|
||||
});
|
||||
|
||||
it('Allows all actions when navigated object not in edit mode', function() {
|
||||
expect(policy.allow(mockAction, context)).toBe(true);
|
||||
});
|
||||
|
||||
it('Allows "window" action when navigated object in edit mode,' +
|
||||
' but selected object not in edit mode ', function() {
|
||||
navigatedObject.hasCapability.andReturn(true);
|
||||
metadata.key = "window";
|
||||
expect(policy.allow(mockAction, context)).toBe(true);
|
||||
});
|
||||
|
||||
it('Allows "remove" action when navigated object in edit mode,' +
|
||||
' and selected object not editable, but its parent is.',
|
||||
function() {
|
||||
var mockParent = jasmine.createSpyObj("parentObject", ["hasCapability"]),
|
||||
mockContextCapability = jasmine.createSpyObj("contextCapability", ["getParent"]);
|
||||
|
||||
mockParent.hasCapability.andReturn(true);
|
||||
mockContextCapability.getParent.andReturn(mockParent);
|
||||
navigatedObject.hasCapability.andReturn(true);
|
||||
|
||||
mockDomainObject.getCapability.andReturn(mockContextCapability);
|
||||
mockDomainObject.hasCapability.andCallFake(function (capability) {
|
||||
switch (capability) {
|
||||
case "editor": return false;
|
||||
case "context": return true;
|
||||
}
|
||||
});
|
||||
metadata.key = "remove";
|
||||
|
||||
expect(policy.allow(mockAction, context)).toBe(true);
|
||||
});
|
||||
|
||||
it('Disallows "move" action when navigated object in edit mode,' +
|
||||
' but selected object not in edit mode ', function() {
|
||||
navigatedObject.hasCapability.andReturn(true);
|
||||
metadata.key = "move";
|
||||
expect(policy.allow(mockAction, context)).toBe(false);
|
||||
});
|
||||
|
||||
it('Disallows copy action when navigated object and' +
|
||||
' selected object in edit mode', function() {
|
||||
navigatedObject.hasCapability.andReturn(true);
|
||||
mockDomainObject.hasCapability.andReturn(true);
|
||||
metadata.key = "copy";
|
||||
expect(policy.allow(mockAction, context)).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -25,7 +25,6 @@ define([
|
||||
"./src/services/PopupService",
|
||||
"./src/SplashScreenManager",
|
||||
"./src/StyleSheetLoader",
|
||||
"./src/UnsupportedBrowserWarning",
|
||||
"./src/controllers/TimeRangeController",
|
||||
"./src/controllers/DateTimePickerController",
|
||||
"./src/controllers/DateTimeFieldController",
|
||||
@@ -74,7 +73,6 @@ define([
|
||||
PopupService,
|
||||
SplashScreenManager,
|
||||
StyleSheetLoader,
|
||||
UnsupportedBrowserWarning,
|
||||
TimeRangeController,
|
||||
DateTimePickerController,
|
||||
DateTimeFieldController,
|
||||
@@ -151,13 +149,6 @@ define([
|
||||
"THEME"
|
||||
]
|
||||
},
|
||||
{
|
||||
"implementation": UnsupportedBrowserWarning,
|
||||
"depends": [
|
||||
"notificationService",
|
||||
"agentService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"implementation": SplashScreenManager,
|
||||
"depends": [
|
||||
@@ -266,8 +257,8 @@ define([
|
||||
"key": "ClickAwayController",
|
||||
"implementation": ClickAwayController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$document"
|
||||
"$document",
|
||||
"$timeout"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -84,7 +84,11 @@ p {
|
||||
margin-bottom: $interiorMarginLg;
|
||||
}
|
||||
|
||||
ol, ul { padding-left: 0; }
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
mct-container {
|
||||
display: block;
|
||||
|
||||
@@ -76,6 +76,11 @@ $pad: $interiorMargin * $baseRatio;
|
||||
font-family: symbolsfont;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
&.t-save-as:before {
|
||||
content:'\e612';
|
||||
font-family: symbolsfont;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
&.t-cancel {
|
||||
.title-label { display: none; }
|
||||
&:before {
|
||||
|
||||
@@ -1,54 +1,42 @@
|
||||
@mixin toiLineHovEffects() {
|
||||
//@include pulse(.25s);
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $timeControllerToiLineColorHov;
|
||||
}
|
||||
}
|
||||
|
||||
mct-include.l-time-controller {
|
||||
.l-time-controller {
|
||||
$minW: 500px;
|
||||
$knobHOffset: 0px;
|
||||
$knobM: ($sliderKnobW + $knobHOffset) * -1;
|
||||
$rangeValPad: $interiorMargin;
|
||||
$rangeValOffset: $sliderKnobW;
|
||||
//$knobCr: $sliderKnobW;
|
||||
$timeRangeSliderLROffset: 130px + $sliderKnobW + $rangeValOffset;
|
||||
$r1H: nth($ueTimeControlH,1);
|
||||
$r2H: nth($ueTimeControlH,2);
|
||||
$r3H: nth($ueTimeControlH,3);
|
||||
|
||||
//@include absPosDefault();
|
||||
//@include test();
|
||||
display: block;
|
||||
//top: auto;
|
||||
height: $r1H + $r2H + $r3H + ($interiorMargin * 2);
|
||||
min-width: $minW;
|
||||
font-size: 0.8rem;
|
||||
|
||||
.l-time-range-inputs-holder,
|
||||
.l-time-range-slider {
|
||||
//font-size: 0.8em;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder,
|
||||
.l-time-range-slider-holder,
|
||||
.l-time-range-ticks-holder
|
||||
{
|
||||
//@include test();
|
||||
@include absPosDefault(0, visible);
|
||||
box-sizing: border-box;
|
||||
top: auto;
|
||||
}
|
||||
.l-time-range-slider,
|
||||
.l-time-range-ticks {
|
||||
//@include test(red, 0.1);
|
||||
@include absPosDefault(0, visible);
|
||||
left: $timeRangeSliderLROffset; right: $timeRangeSliderLROffset;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
//@include test(red);
|
||||
height: $r1H; bottom: $r2H + $r3H + ($interiorMarginSm * 2);
|
||||
padding-top: $interiorMargin;
|
||||
border-top: 1px solid $colorInteriorBorder;
|
||||
@@ -70,7 +58,6 @@ mct-include.l-time-controller {
|
||||
}
|
||||
|
||||
.l-time-range-slider-holder {
|
||||
//@include test(green);
|
||||
height: $r2H; bottom: $r3H + ($interiorMarginSm * 1);
|
||||
.range-holder {
|
||||
box-shadow: none;
|
||||
@@ -82,7 +69,6 @@ mct-include.l-time-controller {
|
||||
$myW: 8px;
|
||||
@include transform(translateX(50%));
|
||||
position: absolute;
|
||||
//@include test();
|
||||
top: 0; right: 0; bottom: 0px; left: auto;
|
||||
width: $myW;
|
||||
height: auto;
|
||||
@@ -97,7 +83,6 @@ mct-include.l-time-controller {
|
||||
// Vert line
|
||||
top: 0; right: auto; bottom: -10px; left: floor($myW/2) - 1;
|
||||
width: 2px;
|
||||
//top: 0; right: 3px; bottom: 0; left: 3px;
|
||||
}
|
||||
&:after {
|
||||
// Circle element
|
||||
@@ -114,7 +99,6 @@ mct-include.l-time-controller {
|
||||
}
|
||||
}
|
||||
&:not(:active) {
|
||||
//@include test(#ff00cc);
|
||||
.knob,
|
||||
.range {
|
||||
@include transition-property(left, right);
|
||||
@@ -155,7 +139,6 @@ mct-include.l-time-controller {
|
||||
.knob {
|
||||
z-index: 2;
|
||||
.range-value {
|
||||
//@include test($sliderColorRange);
|
||||
@include trans-prop-nice-fade(.25s);
|
||||
padding: 0 $rangeValOffset;
|
||||
position: absolute;
|
||||
@@ -167,7 +150,6 @@ mct-include.l-time-controller {
|
||||
color: $sliderColorKnobHov;
|
||||
}
|
||||
&.knob-l {
|
||||
//border-bottom-left-radius: $knobCr; // MOVED TO _CONTROLS.SCSS
|
||||
margin-left: $knobM;
|
||||
.range-value {
|
||||
text-align: right;
|
||||
@@ -175,7 +157,6 @@ mct-include.l-time-controller {
|
||||
}
|
||||
}
|
||||
&.knob-r {
|
||||
//border-bottom-right-radius: $knobCr;
|
||||
margin-right: $knobM;
|
||||
.range-value {
|
||||
left: $rangeValOffset;
|
||||
@@ -185,15 +166,189 @@ mct-include.l-time-controller {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.l-time-domain-selector {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 46px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//.slot.range-holder {
|
||||
// background-color: $sliderColorRangeHolder;
|
||||
//}
|
||||
|
||||
.s-time-range-val {
|
||||
//@include test();
|
||||
border-radius: $controlCr;
|
||||
background-color: $colorInputBg;
|
||||
padding: 1px 1px 0 $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
@include phoneandtablet {
|
||||
.l-time-controller, .l-time-range-inputs-holder {
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
.l-time-controller {
|
||||
|
||||
.l-time-domain-selector {
|
||||
select {
|
||||
height: 25px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.l-time-range-slider-holder, .l-time-range-ticks-holder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.time-range-start, .time-range-end, {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-input {
|
||||
display: block;
|
||||
.s-btn {
|
||||
padding-right: 18px;
|
||||
white-space: nowrap;
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.l-time-range-inputs-elem {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include phone {
|
||||
.l-time-controller {
|
||||
height: 48px;
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
bottom: 24px;
|
||||
}
|
||||
|
||||
.l-time-domain-selector {
|
||||
width: 33%;
|
||||
bottom: -9px;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-input {
|
||||
margin-bottom: 5px;
|
||||
.s-btn {
|
||||
width: 66%;
|
||||
}
|
||||
}
|
||||
.l-time-range-inputs-elem {
|
||||
&.ui-symbol {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.lbl {
|
||||
width: 33%;
|
||||
right: 0px;
|
||||
top: 5px;
|
||||
display: block;
|
||||
height: 25px;
|
||||
margin: 0;
|
||||
line-height: 25px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@include tablet {
|
||||
.l-time-controller {
|
||||
height: 17px;
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
bottom: -7px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.l-time-domain-selector {
|
||||
width: 23%;
|
||||
right: -4px;
|
||||
bottom: -10px;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-input {
|
||||
float: left;
|
||||
.s-btn {
|
||||
width: 100%;
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include tabletLandscape {
|
||||
.l-time-controller {
|
||||
height: 17px;
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
bottom: -7px;
|
||||
}
|
||||
|
||||
.l-time-domain-selector {
|
||||
width: 23%;
|
||||
right: auto;
|
||||
bottom: -10px;
|
||||
left: 391px;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-inputs-elem {
|
||||
&.ui-symbol, &.lbl {
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pane-tree-hidden .l-time-controller {
|
||||
.l-time-domain-selector {
|
||||
left: 667px;
|
||||
}
|
||||
.l-time-range-inputs-holder {
|
||||
padding-left: 277px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@include tabletPortrait {
|
||||
.l-time-controller {
|
||||
height: 17px;
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
bottom: -7px;
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.l-time-domain-selector {
|
||||
width: 23%;
|
||||
right: -4px;
|
||||
bottom: -10px;
|
||||
}
|
||||
|
||||
.l-time-range-inputs-holder {
|
||||
.l-time-range-input {
|
||||
width: 38%;
|
||||
float: left;
|
||||
}
|
||||
.l-time-range-inputs-elem {
|
||||
&.ui-symbol, &.lbl {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,15 +19,18 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div ng-controller="TimeRangeController">
|
||||
<div ng-controller="TimeRangeController as trCtrl">
|
||||
<form class="l-time-range-inputs-holder"
|
||||
ng-submit="updateBoundsFromForm()">
|
||||
ng-submit="trCtrl.updateBoundsFromForm()">
|
||||
<span class="l-time-range-inputs-elem ui-symbol type-icon">C</span>
|
||||
<span class="l-time-range-input">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{ format: parameters.format, validate: validateStart }"
|
||||
structure="{
|
||||
format: parameters.format,
|
||||
validate: trCtrl.validateStart
|
||||
}"
|
||||
ng-model="formModel"
|
||||
ng-blur="updateBoundsFromForm()"
|
||||
ng-blur="trCtrl.updateBoundsFromForm()"
|
||||
field="'start'"
|
||||
class="time-range-start">
|
||||
</mct-control>
|
||||
@@ -37,9 +40,12 @@
|
||||
|
||||
<span class="l-time-range-input" ng-controller="ToggleController as t2">
|
||||
<mct-control key="'datetime-field'"
|
||||
structure="{ format: parameters.format, validate: validateEnd }"
|
||||
structure="{
|
||||
format: parameters.format,
|
||||
validate: trCtrl.validateEnd
|
||||
}"
|
||||
ng-model="formModel"
|
||||
ng-blur="updateBoundsFromForm()"
|
||||
ng-blur="trCtrl.updateBoundsFromForm()"
|
||||
field="'end'"
|
||||
class="time-range-end">
|
||||
</mct-control>
|
||||
@@ -53,22 +59,25 @@
|
||||
<div class="slider"
|
||||
mct-resize="spanWidth = bounds.width">
|
||||
<div class="knob knob-l"
|
||||
mct-drag-down="startLeftDrag()"
|
||||
mct-drag="leftDrag(delta[0])"
|
||||
mct-drag-down="trCtrl.startLeftDrag()"
|
||||
mct-drag="trCtrl.leftDrag(delta[0])"
|
||||
ng-style="{ left: startInnerPct }">
|
||||
<div class="range-value">{{startInnerText}}</div>
|
||||
</div>
|
||||
<div class="knob knob-r"
|
||||
mct-drag-down="startRightDrag()"
|
||||
mct-drag="rightDrag(delta[0])"
|
||||
mct-drag-down="trCtrl.startRightDrag()"
|
||||
mct-drag="trCtrl.rightDrag(delta[0])"
|
||||
ng-style="{ right: endInnerPct }">
|
||||
<div class="range-value">{{endInnerText}}</div>
|
||||
</div>
|
||||
<div class="slot range-holder">
|
||||
<div class="range"
|
||||
mct-drag-down="startMiddleDrag()"
|
||||
mct-drag="middleDrag(delta[0])"
|
||||
ng-style="{ left: startInnerPct, right: endInnerPct}">
|
||||
mct-drag-down="trCtrl.startMiddleDrag()"
|
||||
mct-drag="trCtrl.middleDrag(delta[0])"
|
||||
ng-style="{
|
||||
left: startInnerPct,
|
||||
right: endInnerPct
|
||||
}">
|
||||
<div class="toi-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* This bundle provides various general-purpose UI elements, including
|
||||
* platform styling.
|
||||
* @namespace platform/commonUI/general
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
var WARNING_TITLE = "Unsupported browser",
|
||||
WARNING_DESCRIPTION = [
|
||||
"This software has been developed and tested",
|
||||
"using the latest Google Chrome,",
|
||||
"and may be unstable in other browsers."
|
||||
].join(" "),
|
||||
MOBILE_BROWSER = "Safari",
|
||||
DESKTOP_BROWSER = "Chrome";
|
||||
|
||||
/**
|
||||
* Shows a warning if a user's browser is unsupported.
|
||||
* @memberof platform/commonUI/general
|
||||
* @constructor
|
||||
* @param {NotificationService} notificationService the notification
|
||||
* service
|
||||
*/
|
||||
function UnsupportedBrowserWarning(notificationService, agentService) {
|
||||
var testToBrowser = agentService.isMobile() ?
|
||||
MOBILE_BROWSER : DESKTOP_BROWSER;
|
||||
|
||||
if (!agentService.isBrowser(testToBrowser)) {
|
||||
notificationService.alert({
|
||||
title: WARNING_TITLE,
|
||||
actionText: WARNING_DESCRIPTION
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return UnsupportedBrowserWarning;
|
||||
}
|
||||
);
|
||||
@@ -34,20 +34,19 @@ define(
|
||||
* @param $scope the scope in which this controller is active
|
||||
* @param $document the document element, injected by Angular
|
||||
*/
|
||||
function ClickAwayController($scope, $document) {
|
||||
function ClickAwayController($document, $timeout) {
|
||||
var self = this;
|
||||
|
||||
this.state = false;
|
||||
this.$scope = $scope;
|
||||
this.$document = $document;
|
||||
|
||||
// Callback used by the document listener. Deactivates;
|
||||
// note also $scope.$apply is invoked to indicate that
|
||||
// the state of this controller has changed.
|
||||
// Callback used by the document listener. Timeout ensures that
|
||||
// `clickaway` action occurs after `toggle` if `toggle` is
|
||||
// triggered by a click/mouseup.
|
||||
this.clickaway = function () {
|
||||
self.deactivate();
|
||||
$scope.$apply();
|
||||
return false;
|
||||
$timeout(function () {
|
||||
self.deactivate();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,244 +20,286 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
define([
|
||||
|
||||
var TICK_SPACING_PX = 150;
|
||||
], function () {
|
||||
|
||||
var TICK_SPACING_PX = 150;
|
||||
|
||||
/**
|
||||
* Controller used by the `time-controller` template.
|
||||
* @memberof platform/commonUI/general
|
||||
* @constructor
|
||||
* @param $scope the Angular scope for this controller
|
||||
* @param {FormatService} formatService the service to user to format
|
||||
* domain values
|
||||
* @param {string} defaultFormat the format to request when no
|
||||
* format has been otherwise specified
|
||||
* @param {Function} now a function to return current system time
|
||||
*/
|
||||
function TimeRangeController($scope, formatService, defaultFormat, now) {
|
||||
var tickCount = 2,
|
||||
innerMinimumSpan = 1000, // 1 second
|
||||
outerMinimumSpan = 1000, // 1 second
|
||||
initialDragValue,
|
||||
formatter = formatService.getFormat(defaultFormat);
|
||||
|
||||
function formatTimestamp(ts) {
|
||||
return formatter.format(ts);
|
||||
}
|
||||
|
||||
// From 0.0-1.0 to "0%"-"100%"
|
||||
function toPercent(p) {
|
||||
return (100 * p) + "%";
|
||||
}
|
||||
|
||||
function updateTicks() {
|
||||
var i, p, ts, start, end, span;
|
||||
end = $scope.ngModel.outer.end;
|
||||
start = $scope.ngModel.outer.start;
|
||||
span = end - start;
|
||||
$scope.ticks = [];
|
||||
for (i = 0; i < tickCount; i += 1) {
|
||||
p = i / (tickCount - 1);
|
||||
ts = p * span + start;
|
||||
$scope.ticks.push(formatTimestamp(ts));
|
||||
}
|
||||
}
|
||||
|
||||
function updateSpanWidth(w) {
|
||||
tickCount = Math.max(Math.floor(w / TICK_SPACING_PX), 2);
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
function updateViewForInnerSpanFromModel(ngModel) {
|
||||
var span = ngModel.outer.end - ngModel.outer.start;
|
||||
|
||||
// Expose readable dates for the knobs
|
||||
$scope.startInnerText = formatTimestamp(ngModel.inner.start);
|
||||
$scope.endInnerText = formatTimestamp(ngModel.inner.end);
|
||||
|
||||
// And positions for the knobs
|
||||
$scope.startInnerPct =
|
||||
toPercent((ngModel.inner.start - ngModel.outer.start) / span);
|
||||
$scope.endInnerPct =
|
||||
toPercent((ngModel.outer.end - ngModel.inner.end) / span);
|
||||
}
|
||||
|
||||
function defaultBounds() {
|
||||
var t = now();
|
||||
return {
|
||||
start: t - 24 * 3600 * 1000, // One day
|
||||
end: t
|
||||
};
|
||||
}
|
||||
|
||||
function copyBounds(bounds) {
|
||||
return { start: bounds.start, end: bounds.end };
|
||||
}
|
||||
|
||||
function updateViewFromModel(ngModel) {
|
||||
ngModel = ngModel || {};
|
||||
ngModel.outer = ngModel.outer || defaultBounds();
|
||||
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
|
||||
|
||||
// Stick it back is scope (in case we just set defaults)
|
||||
$scope.ngModel = ngModel;
|
||||
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
function startLeftDrag() {
|
||||
initialDragValue = $scope.ngModel.inner.start;
|
||||
}
|
||||
|
||||
function startRightDrag() {
|
||||
initialDragValue = $scope.ngModel.inner.end;
|
||||
}
|
||||
|
||||
function startMiddleDrag() {
|
||||
initialDragValue = {
|
||||
start: $scope.ngModel.inner.start,
|
||||
end: $scope.ngModel.inner.end
|
||||
};
|
||||
}
|
||||
|
||||
function toMillis(pixels) {
|
||||
var span =
|
||||
$scope.ngModel.outer.end - $scope.ngModel.outer.start;
|
||||
return (pixels / $scope.spanWidth) * span;
|
||||
}
|
||||
|
||||
function clamp(value, low, high) {
|
||||
return Math.max(low, Math.min(high, value));
|
||||
}
|
||||
|
||||
function leftDrag(pixels) {
|
||||
var delta = toMillis(pixels);
|
||||
$scope.ngModel.inner.start = clamp(
|
||||
initialDragValue + delta,
|
||||
$scope.ngModel.outer.start,
|
||||
$scope.ngModel.inner.end - innerMinimumSpan
|
||||
);
|
||||
updateViewFromModel($scope.ngModel);
|
||||
}
|
||||
|
||||
function rightDrag(pixels) {
|
||||
var delta = toMillis(pixels);
|
||||
$scope.ngModel.inner.end = clamp(
|
||||
initialDragValue + delta,
|
||||
$scope.ngModel.inner.start + innerMinimumSpan,
|
||||
$scope.ngModel.outer.end
|
||||
);
|
||||
updateViewFromModel($scope.ngModel);
|
||||
}
|
||||
|
||||
function middleDrag(pixels) {
|
||||
var delta = toMillis(pixels),
|
||||
edge = delta < 0 ? 'start' : 'end',
|
||||
opposite = delta < 0 ? 'end' : 'start';
|
||||
|
||||
// Adjust the position of the edge in the direction of drag
|
||||
$scope.ngModel.inner[edge] = clamp(
|
||||
initialDragValue[edge] + delta,
|
||||
$scope.ngModel.outer.start,
|
||||
$scope.ngModel.outer.end
|
||||
);
|
||||
// Adjust opposite knob to maintain span
|
||||
$scope.ngModel.inner[opposite] = $scope.ngModel.inner[edge] +
|
||||
initialDragValue[opposite] - initialDragValue[edge];
|
||||
|
||||
updateViewFromModel($scope.ngModel);
|
||||
}
|
||||
|
||||
function updateFormModel() {
|
||||
$scope.formModel = {
|
||||
start: (($scope.ngModel || {}).outer || {}).start,
|
||||
end: (($scope.ngModel || {}).outer || {}).end
|
||||
};
|
||||
}
|
||||
|
||||
function updateOuterStart() {
|
||||
var ngModel = $scope.ngModel;
|
||||
|
||||
ngModel.inner.start =
|
||||
Math.max(ngModel.outer.start, ngModel.inner.start);
|
||||
ngModel.inner.end = Math.max(
|
||||
ngModel.inner.start + innerMinimumSpan,
|
||||
ngModel.inner.end
|
||||
);
|
||||
|
||||
updateFormModel();
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
function updateOuterEnd() {
|
||||
var ngModel = $scope.ngModel;
|
||||
|
||||
ngModel.inner.end =
|
||||
Math.min(ngModel.outer.end, ngModel.inner.end);
|
||||
ngModel.inner.start = Math.min(
|
||||
ngModel.inner.end - innerMinimumSpan,
|
||||
ngModel.inner.start
|
||||
);
|
||||
|
||||
updateFormModel();
|
||||
updateViewForInnerSpanFromModel(ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
function updateFormat(key) {
|
||||
formatter = formatService.getFormat(key || defaultFormat);
|
||||
updateViewForInnerSpanFromModel($scope.ngModel);
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
function updateBoundsFromForm() {
|
||||
var start = $scope.formModel.start,
|
||||
end = $scope.formModel.end;
|
||||
if (end >= start + outerMinimumSpan) {
|
||||
$scope.ngModel = $scope.ngModel || {};
|
||||
$scope.ngModel.outer = { start: start, end: end };
|
||||
}
|
||||
}
|
||||
|
||||
function validateStart(startValue) {
|
||||
return startValue <= $scope.formModel.end - outerMinimumSpan;
|
||||
}
|
||||
|
||||
function validateEnd(endValue) {
|
||||
return endValue >= $scope.formModel.start + outerMinimumSpan;
|
||||
}
|
||||
|
||||
$scope.startLeftDrag = startLeftDrag;
|
||||
$scope.startRightDrag = startRightDrag;
|
||||
$scope.startMiddleDrag = startMiddleDrag;
|
||||
$scope.leftDrag = leftDrag;
|
||||
$scope.rightDrag = rightDrag;
|
||||
$scope.middleDrag = middleDrag;
|
||||
|
||||
$scope.updateBoundsFromForm = updateBoundsFromForm;
|
||||
|
||||
$scope.validateStart = validateStart;
|
||||
$scope.validateEnd = validateEnd;
|
||||
|
||||
$scope.ticks = [];
|
||||
|
||||
// Initialize scope to defaults
|
||||
updateViewFromModel($scope.ngModel);
|
||||
updateFormModel();
|
||||
|
||||
$scope.$watchCollection("ngModel", updateViewFromModel);
|
||||
$scope.$watch("spanWidth", updateSpanWidth);
|
||||
$scope.$watch("ngModel.outer.start", updateOuterStart);
|
||||
$scope.$watch("ngModel.outer.end", updateOuterEnd);
|
||||
$scope.$watch("parameters.format", updateFormat);
|
||||
}
|
||||
|
||||
return TimeRangeController;
|
||||
/* format number as percent; 0.0-1.0 to "0%"-"100%" */
|
||||
function toPercent(p) {
|
||||
return (100 * p) + "%";
|
||||
}
|
||||
);
|
||||
|
||||
function clamp(value, low, high) {
|
||||
return Math.max(low, Math.min(high, value));
|
||||
}
|
||||
|
||||
function copyBounds(bounds) {
|
||||
return {
|
||||
start: bounds.start,
|
||||
end: bounds.end
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller used by the `time-controller` template.
|
||||
* @memberof platform/commonUI/general
|
||||
* @constructor
|
||||
* @param $scope the Angular scope for this controller
|
||||
* @param {FormatService} formatService the service to user to format
|
||||
* domain values
|
||||
* @param {string} defaultFormat the format to request when no
|
||||
* format has been otherwise specified
|
||||
* @param {Function} now a function to return current system time
|
||||
*/
|
||||
function TimeRangeController($scope, formatService, defaultFormat, now) {
|
||||
this.$scope = $scope;
|
||||
this.formatService = formatService;
|
||||
this.defaultFormat = defaultFormat;
|
||||
this.now = now;
|
||||
|
||||
this.tickCount = 2;
|
||||
this.innerMinimumSpan = 1000; // 1 second
|
||||
this.outerMinimumSpan = 1000; // 1 second
|
||||
this.initialDragValue = undefined;
|
||||
this.formatter = formatService.getFormat(defaultFormat);
|
||||
this.formStartChanged = false;
|
||||
this.formEndChanged = false;
|
||||
|
||||
this.$scope.ticks = [];
|
||||
|
||||
this.updateViewFromModel(this.$scope.ngModel);
|
||||
this.updateFormModel();
|
||||
|
||||
[
|
||||
'updateViewFromModel',
|
||||
'updateSpanWidth',
|
||||
'updateOuterStart',
|
||||
'updateOuterEnd',
|
||||
'updateFormat',
|
||||
'validateStart',
|
||||
'validateEnd',
|
||||
'onFormStartChange',
|
||||
'onFormEndChange'
|
||||
].forEach(function (boundFn) {
|
||||
this[boundFn] = this[boundFn].bind(this);
|
||||
}, this);
|
||||
|
||||
this.$scope.$watchCollection("ngModel", this.updateViewFromModel);
|
||||
this.$scope.$watch("spanWidth", this.updateSpanWidth);
|
||||
this.$scope.$watch("ngModel.outer.start", this.updateOuterStart);
|
||||
this.$scope.$watch("ngModel.outer.end", this.updateOuterEnd);
|
||||
this.$scope.$watch("parameters.format", this.updateFormat);
|
||||
this.$scope.$watch("formModel.start", this.onFormStartChange);
|
||||
this.$scope.$watch("formModel.end", this.onFormEndChange);
|
||||
}
|
||||
|
||||
TimeRangeController.prototype.formatTimestamp = function (ts) {
|
||||
return this.formatter.format(ts);
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.updateTicks = function () {
|
||||
var i, p, ts, start, end, span;
|
||||
end = this.$scope.ngModel.outer.end;
|
||||
start = this.$scope.ngModel.outer.start;
|
||||
span = end - start;
|
||||
this.$scope.ticks = [];
|
||||
for (i = 0; i < this.tickCount; i += 1) {
|
||||
p = i / (this.tickCount - 1);
|
||||
ts = p * span + start;
|
||||
this.$scope.ticks.push(this.formatTimestamp(ts));
|
||||
}
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.updateSpanWidth = function (w) {
|
||||
this.tickCount = Math.max(Math.floor(w / TICK_SPACING_PX), 2);
|
||||
this.updateTicks();
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.updateViewForInnerSpanFromModel = function (
|
||||
ngModel
|
||||
) {
|
||||
var span = ngModel.outer.end - ngModel.outer.start;
|
||||
|
||||
// Expose readable dates for the knobs
|
||||
this.$scope.startInnerText = this.formatTimestamp(ngModel.inner.start);
|
||||
this.$scope.endInnerText = this.formatTimestamp(ngModel.inner.end);
|
||||
|
||||
// And positions for the knobs
|
||||
this.$scope.startInnerPct =
|
||||
toPercent((ngModel.inner.start - ngModel.outer.start) / span);
|
||||
this.$scope.endInnerPct =
|
||||
toPercent((ngModel.outer.end - ngModel.inner.end) / span);
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.defaultBounds = function () {
|
||||
var t = this.now();
|
||||
return {
|
||||
start: t - 24 * 3600 * 1000, // One day
|
||||
end: t
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
TimeRangeController.prototype.updateViewFromModel = function (ngModel) {
|
||||
ngModel = ngModel || {};
|
||||
ngModel.outer = ngModel.outer || this.defaultBounds();
|
||||
ngModel.inner = ngModel.inner || copyBounds(ngModel.outer);
|
||||
|
||||
// Stick it back is scope (in case we just set defaults)
|
||||
this.$scope.ngModel = ngModel;
|
||||
|
||||
this.updateViewForInnerSpanFromModel(ngModel);
|
||||
this.updateTicks();
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.startLeftDrag = function () {
|
||||
this.initialDragValue = this.$scope.ngModel.inner.start;
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.startRightDrag = function () {
|
||||
this.initialDragValue = this.$scope.ngModel.inner.end;
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.startMiddleDrag = function () {
|
||||
this.initialDragValue = {
|
||||
start: this.$scope.ngModel.inner.start,
|
||||
end: this.$scope.ngModel.inner.end
|
||||
};
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.toMillis = function (pixels) {
|
||||
var span =
|
||||
this.$scope.ngModel.outer.end - this.$scope.ngModel.outer.start;
|
||||
return (pixels / this.$scope.spanWidth) * span;
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.leftDrag = function (pixels) {
|
||||
var delta = this.toMillis(pixels);
|
||||
this.$scope.ngModel.inner.start = clamp(
|
||||
this.initialDragValue + delta,
|
||||
this.$scope.ngModel.outer.start,
|
||||
this.$scope.ngModel.inner.end - this.innerMinimumSpan
|
||||
);
|
||||
this.updateViewFromModel(this.$scope.ngModel);
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.rightDrag = function (pixels) {
|
||||
var delta = this.toMillis(pixels);
|
||||
this.$scope.ngModel.inner.end = clamp(
|
||||
this.initialDragValue + delta,
|
||||
this.$scope.ngModel.inner.start + this.innerMinimumSpan,
|
||||
this.$scope.ngModel.outer.end
|
||||
);
|
||||
this.updateViewFromModel(this.$scope.ngModel);
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.middleDrag = function (pixels) {
|
||||
var delta = this.toMillis(pixels),
|
||||
edge = delta < 0 ? 'start' : 'end',
|
||||
opposite = delta < 0 ? 'end' : 'start';
|
||||
|
||||
// Adjust the position of the edge in the direction of drag
|
||||
this.$scope.ngModel.inner[edge] = clamp(
|
||||
this.initialDragValue[edge] + delta,
|
||||
this.$scope.ngModel.outer.start,
|
||||
this.$scope.ngModel.outer.end
|
||||
);
|
||||
// Adjust opposite knob to maintain span
|
||||
this.$scope.ngModel.inner[opposite] =
|
||||
this.$scope.ngModel.inner[edge] +
|
||||
this.initialDragValue[opposite] -
|
||||
this.initialDragValue[edge];
|
||||
|
||||
this.updateViewFromModel(this.$scope.ngModel);
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.updateFormModel = function () {
|
||||
this.$scope.formModel = {
|
||||
start: ((this.$scope.ngModel || {}).outer || {}).start,
|
||||
end: ((this.$scope.ngModel || {}).outer || {}).end
|
||||
};
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.updateOuterStart = function () {
|
||||
var ngModel = this.$scope.ngModel;
|
||||
|
||||
ngModel.inner.start =
|
||||
Math.max(ngModel.outer.start, ngModel.inner.start);
|
||||
ngModel.inner.end = Math.max(
|
||||
ngModel.inner.start + this.innerMinimumSpan,
|
||||
ngModel.inner.end
|
||||
);
|
||||
|
||||
this.updateFormModel();
|
||||
this.updateViewForInnerSpanFromModel(ngModel);
|
||||
this.updateTicks();
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.updateOuterEnd = function () {
|
||||
var ngModel = this.$scope.ngModel;
|
||||
|
||||
ngModel.inner.end =
|
||||
Math.min(ngModel.outer.end, ngModel.inner.end);
|
||||
ngModel.inner.start = Math.min(
|
||||
ngModel.inner.end - this.innerMinimumSpan,
|
||||
ngModel.inner.start
|
||||
);
|
||||
|
||||
this.updateFormModel();
|
||||
this.updateViewForInnerSpanFromModel(ngModel);
|
||||
this.updateTicks();
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.updateFormat = function (key) {
|
||||
this.formatter = this.formatService.getFormat(key || this.defaultFormat);
|
||||
this.updateViewForInnerSpanFromModel(this.$scope.ngModel);
|
||||
this.updateTicks();
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.updateBoundsFromForm = function () {
|
||||
if (this.formStartChanged) {
|
||||
this.$scope.ngModel.outer.start =
|
||||
this.$scope.ngModel.inner.start =
|
||||
this.$scope.formModel.start;
|
||||
this.formStartChanged = false;
|
||||
}
|
||||
if (this.formEndChanged) {
|
||||
this.$scope.ngModel.outer.end =
|
||||
this.$scope.ngModel.inner.end =
|
||||
this.$scope.formModel.end;
|
||||
this.formEndChanged = false;
|
||||
}
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.onFormStartChange = function (
|
||||
newValue,
|
||||
oldValue
|
||||
) {
|
||||
if (!this.formStartChanged && newValue !== oldValue) {
|
||||
this.formStartChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.onFormEndChange = function (
|
||||
newValue,
|
||||
oldValue
|
||||
) {
|
||||
if (!this.formEndChanged && newValue !== oldValue) {
|
||||
this.formEndChanged = true;
|
||||
}
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.validateStart = function (startValue) {
|
||||
return startValue <=
|
||||
this.$scope.formModel.end - this.outerMinimumSpan;
|
||||
};
|
||||
|
||||
TimeRangeController.prototype.validateEnd = function (endValue) {
|
||||
return endValue >=
|
||||
this.$scope.formModel.start + this.outerMinimumSpan;
|
||||
};
|
||||
|
||||
return TimeRangeController;
|
||||
});
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/UnsupportedBrowserWarning"],
|
||||
function (UnsupportedBrowserWarning) {
|
||||
|
||||
var MOBILE_BROWSER = "Safari",
|
||||
DESKTOP_BROWSER = "Chrome",
|
||||
UNSUPPORTED_BROWSERS = [
|
||||
"Firefox",
|
||||
"IE",
|
||||
"Opera",
|
||||
"Iceweasel"
|
||||
];
|
||||
|
||||
describe("The unsupported browser warning", function () {
|
||||
var mockNotificationService,
|
||||
mockAgentService,
|
||||
testAgent;
|
||||
|
||||
function instantiateWith(browser) {
|
||||
testAgent = "Mozilla/5.0 " + browser + "/12.34.56";
|
||||
return new UnsupportedBrowserWarning(
|
||||
mockNotificationService,
|
||||
mockAgentService
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
testAgent = "chrome";
|
||||
mockNotificationService = jasmine.createSpyObj(
|
||||
"notificationService",
|
||||
[ "alert" ]
|
||||
);
|
||||
mockAgentService = jasmine.createSpyObj(
|
||||
"agentService",
|
||||
[ "isMobile", "isBrowser" ]
|
||||
);
|
||||
mockAgentService.isBrowser.andCallFake(function (substr) {
|
||||
substr = substr.toLowerCase();
|
||||
return testAgent.toLowerCase().indexOf(substr) !== -1;
|
||||
});
|
||||
});
|
||||
|
||||
[ false, true ].forEach(function (isMobile) {
|
||||
var deviceType = isMobile ? "mobile" : "desktop",
|
||||
goodBrowser = isMobile ? MOBILE_BROWSER : DESKTOP_BROWSER,
|
||||
badBrowsers = UNSUPPORTED_BROWSERS.concat([
|
||||
isMobile ? DESKTOP_BROWSER : MOBILE_BROWSER
|
||||
]);
|
||||
|
||||
describe("on " + deviceType + " devices", function () {
|
||||
beforeEach(function () {
|
||||
mockAgentService.isMobile.andReturn(isMobile);
|
||||
});
|
||||
|
||||
it("is not shown for " + goodBrowser, function () {
|
||||
instantiateWith(goodBrowser);
|
||||
expect(mockNotificationService.alert)
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
badBrowsers.forEach(function (badBrowser) {
|
||||
it("is shown for " + badBrowser, function () {
|
||||
instantiateWith(badBrowser);
|
||||
expect(mockNotificationService.alert)
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -25,20 +25,20 @@ define(
|
||||
function (ClickAwayController) {
|
||||
|
||||
describe("The click-away controller", function () {
|
||||
var mockScope,
|
||||
mockDocument,
|
||||
var mockDocument,
|
||||
mockTimeout,
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$apply" ]
|
||||
);
|
||||
mockDocument = jasmine.createSpyObj(
|
||||
"$document",
|
||||
[ "on", "off" ]
|
||||
);
|
||||
controller = new ClickAwayController(mockScope, mockDocument);
|
||||
mockTimeout = jasmine.createSpy('timeout');
|
||||
controller = new ClickAwayController(
|
||||
mockDocument,
|
||||
mockTimeout
|
||||
);
|
||||
});
|
||||
|
||||
it("is initially inactive", function () {
|
||||
@@ -77,10 +77,12 @@ define(
|
||||
});
|
||||
|
||||
it("deactivates and detaches listener on document click", function () {
|
||||
var callback;
|
||||
var callback, timeout;
|
||||
controller.setState(true);
|
||||
callback = mockDocument.on.mostRecentCall.args[1];
|
||||
callback();
|
||||
timeout = mockTimeout.mostRecentCall.args[0];
|
||||
timeout();
|
||||
expect(controller.isActive()).toEqual(false);
|
||||
expect(mockDocument.off).toHaveBeenCalledWith("mouseup", callback);
|
||||
});
|
||||
@@ -89,4 +91,4 @@ define(
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
@@ -92,18 +92,18 @@ define(
|
||||
it("exposes start time validator", function () {
|
||||
var testValue = 42000000;
|
||||
mockScope.formModel = { end: testValue };
|
||||
expect(mockScope.validateStart(testValue + 1))
|
||||
expect(controller.validateStart(testValue + 1))
|
||||
.toBe(false);
|
||||
expect(mockScope.validateStart(testValue - 60 * 60 * 1000 - 1))
|
||||
expect(controller.validateStart(testValue - 60 * 60 * 1000 - 1))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
it("exposes end time validator", function () {
|
||||
var testValue = 42000000;
|
||||
mockScope.formModel = { start: testValue };
|
||||
expect(mockScope.validateEnd(testValue - 1))
|
||||
expect(controller.validateEnd(testValue - 1))
|
||||
.toBe(false);
|
||||
expect(mockScope.validateEnd(testValue + 60 * 60 * 1000 + 1))
|
||||
expect(controller.validateEnd(testValue + 60 * 60 * 1000 + 1))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
@@ -117,25 +117,87 @@ define(
|
||||
start: DAY * 10000,
|
||||
end: DAY * 11000
|
||||
};
|
||||
// These watches may not exist, but Angular would fire
|
||||
// them if they did.
|
||||
});
|
||||
|
||||
it('updates all changed bounds when requested', function () {
|
||||
fireWatchCollection("formModel", mockScope.formModel);
|
||||
fireWatch("formModel.start", mockScope.formModel.start);
|
||||
fireWatch("formModel.end", mockScope.formModel.end);
|
||||
});
|
||||
|
||||
it("does not immediately make changes to the model", function () {
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.not.toEqual(mockScope.formModel.start);
|
||||
expect(mockScope.ngModel.inner.start)
|
||||
.not.toEqual(mockScope.formModel.start);
|
||||
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.not.toEqual(mockScope.formModel.end);
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.not.toEqual(mockScope.formModel.end);
|
||||
|
||||
controller.updateBoundsFromForm();
|
||||
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.toEqual(mockScope.formModel.start);
|
||||
expect(mockScope.ngModel.inner.start)
|
||||
.toEqual(mockScope.formModel.start);
|
||||
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.toEqual(mockScope.formModel.end);
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.toEqual(mockScope.formModel.end);
|
||||
});
|
||||
|
||||
it('updates changed start bound when requested', function () {
|
||||
fireWatchCollection("formModel", mockScope.formModel);
|
||||
fireWatch("formModel.start", mockScope.formModel.start);
|
||||
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.not.toEqual(mockScope.formModel.start);
|
||||
expect(mockScope.ngModel.inner.start)
|
||||
.not.toEqual(mockScope.formModel.start);
|
||||
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.not.toEqual(mockScope.formModel.end);
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.not.toEqual(mockScope.formModel.end);
|
||||
|
||||
controller.updateBoundsFromForm();
|
||||
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.toEqual(mockScope.formModel.start);
|
||||
expect(mockScope.ngModel.inner.start)
|
||||
.toEqual(mockScope.formModel.start);
|
||||
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.not.toEqual(mockScope.formModel.end);
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.not.toEqual(mockScope.formModel.end);
|
||||
});
|
||||
|
||||
it("updates model bounds on request", function () {
|
||||
mockScope.updateBoundsFromForm();
|
||||
it('updates changed end bound when requested', function () {
|
||||
fireWatchCollection("formModel", mockScope.formModel);
|
||||
fireWatch("formModel.end", mockScope.formModel.end);
|
||||
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.toEqual(mockScope.formModel.start);
|
||||
.not.toEqual(mockScope.formModel.start);
|
||||
expect(mockScope.ngModel.inner.start)
|
||||
.not.toEqual(mockScope.formModel.start);
|
||||
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.not.toEqual(mockScope.formModel.end);
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.not.toEqual(mockScope.formModel.end);
|
||||
|
||||
controller.updateBoundsFromForm();
|
||||
|
||||
expect(mockScope.ngModel.outer.start)
|
||||
.not.toEqual(mockScope.formModel.start);
|
||||
expect(mockScope.ngModel.inner.start)
|
||||
.not.toEqual(mockScope.formModel.start);
|
||||
|
||||
expect(mockScope.ngModel.outer.end)
|
||||
.toEqual(mockScope.formModel.end);
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.toEqual(mockScope.formModel.end);
|
||||
});
|
||||
});
|
||||
@@ -158,27 +220,27 @@ define(
|
||||
});
|
||||
|
||||
it("updates the start time for left drags", function () {
|
||||
mockScope.startLeftDrag();
|
||||
mockScope.leftDrag(250);
|
||||
controller.startLeftDrag();
|
||||
controller.leftDrag(250);
|
||||
expect(mockScope.ngModel.inner.start)
|
||||
.toEqual(DAY * 1000 + HOUR * 9);
|
||||
});
|
||||
|
||||
it("updates the end time for right drags", function () {
|
||||
mockScope.startRightDrag();
|
||||
mockScope.rightDrag(-250);
|
||||
controller.startRightDrag();
|
||||
controller.rightDrag(-250);
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.toEqual(DAY * 1000 + HOUR * 15);
|
||||
});
|
||||
|
||||
it("updates both start and end for middle drags", function () {
|
||||
mockScope.startMiddleDrag();
|
||||
mockScope.middleDrag(-125);
|
||||
controller.startMiddleDrag();
|
||||
controller.middleDrag(-125);
|
||||
expect(mockScope.ngModel.inner).toEqual({
|
||||
start: DAY * 1000,
|
||||
end: DAY * 1000 + HOUR * 18
|
||||
});
|
||||
mockScope.middleDrag(250);
|
||||
controller.middleDrag(250);
|
||||
expect(mockScope.ngModel.inner).toEqual({
|
||||
start: DAY * 1000 + HOUR * 6,
|
||||
end: DAY * 1001
|
||||
@@ -186,8 +248,8 @@ define(
|
||||
});
|
||||
|
||||
it("enforces a minimum inner span", function () {
|
||||
mockScope.startRightDrag();
|
||||
mockScope.rightDrag(-9999999);
|
||||
controller.startRightDrag();
|
||||
controller.rightDrag(-9999999);
|
||||
expect(mockScope.ngModel.inner.end)
|
||||
.toBeGreaterThan(mockScope.ngModel.inner.start);
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*global define,describe,beforeEach,jasmine,it,expect*/
|
||||
/*global describe,beforeEach,jasmine,it,expect*/
|
||||
|
||||
define([
|
||||
'../../src/ui/TreeView',
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
var DISALLOWED_ACTIONS = ["copy", "window", "follow"];
|
||||
|
||||
/**
|
||||
* The ActionCapability allows applicable Actions to be retrieved and
|
||||
@@ -53,37 +52,22 @@ define(
|
||||
this.domainObject = domainObject;
|
||||
}
|
||||
|
||||
function isEditable(domainObject){
|
||||
return domainObject.getCapability('status').get('editing');
|
||||
}
|
||||
|
||||
function hasEditableAncestor(domainObject){
|
||||
return domainObject.hasCapability('context') &&
|
||||
domainObject
|
||||
.getCapability('context')
|
||||
.getPath()
|
||||
.some(function isEditable (ancestor){
|
||||
return ancestor.getCapability('status').get('editing');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the actions applicable to the domain object in the given
|
||||
* context.
|
||||
* Perform an action. This will find and perform the
|
||||
* first matching action available for the specified
|
||||
* context or key.
|
||||
*
|
||||
* @param {ActionContext|string} context the context in which
|
||||
* to assess the applicability of the available actions; this is
|
||||
* passed along to the action service to match against available
|
||||
* to perform the action; this is passed along to
|
||||
* the action service to match against available
|
||||
* actions. The "domainObject" field will automatically
|
||||
* be populated with the domain object that exposed
|
||||
* this capability. If given as a string, this will
|
||||
* be taken as the "key" field to match against
|
||||
* specific actions.
|
||||
*
|
||||
* Additionally, this function will limit the actions
|
||||
* available for an object in Edit Mode
|
||||
* @returns {Array<Action>} The actions applicable to this domain
|
||||
* object in the given context
|
||||
* @returns {Promise} the result of the action that was
|
||||
* performed, or undefined if no matching action
|
||||
* was found.
|
||||
* @memberof platform/core.ActionCapability#
|
||||
*/
|
||||
ActionCapability.prototype.getActions = function (context) {
|
||||
@@ -92,19 +76,11 @@ define(
|
||||
// but additionally adds a domainObject field.
|
||||
var baseContext = typeof context === 'string' ?
|
||||
{ key: context } : (context || {}),
|
||||
actionContext = Object.create(baseContext),
|
||||
actions;
|
||||
actionContext = Object.create(baseContext);
|
||||
|
||||
actionContext.domainObject = this.domainObject;
|
||||
|
||||
actions = this.actionService.getActions(actionContext) || [];
|
||||
if (isEditable(this.domainObject) || hasEditableAncestor(this.domainObject)){
|
||||
return actions.filter(function(action){
|
||||
return DISALLOWED_ACTIONS.indexOf(action.getMetadata().key) === -1;
|
||||
});
|
||||
} else {
|
||||
return actions;
|
||||
}
|
||||
return this.actionService.getActions(actionContext);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
define(
|
||||
["../../src/actions/ActionCapability"],
|
||||
function (ActionCapability) {
|
||||
//TODO: Disabled for NEM beta
|
||||
xdescribe("The action capability", function () {
|
||||
|
||||
describe("The action capability", function () {
|
||||
var mockQ,
|
||||
mockAction,
|
||||
mockActionService,
|
||||
@@ -95,4 +95,4 @@ define(
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
ng-model='ngModel'
|
||||
field="'domain'"
|
||||
options="ngModel.options"
|
||||
style="position: absolute; right: 0px; bottom: 46px;"
|
||||
class="l-time-domain-selector"
|
||||
>
|
||||
</mct-control>
|
||||
|
||||
@@ -22,16 +22,16 @@
|
||||
|
||||
define([
|
||||
"./src/directives/MCTTable",
|
||||
"./src/controllers/RTTelemetryTableController",
|
||||
"./src/controllers/TelemetryTableController",
|
||||
"./src/controllers/RealtimeTableController",
|
||||
"./src/controllers/HistoricalTableController",
|
||||
"./src/controllers/TableOptionsController",
|
||||
'../../commonUI/regions/src/Region',
|
||||
'../../commonUI/browse/src/InspectorRegion',
|
||||
"legacyRegistry"
|
||||
], function (
|
||||
MCTTable,
|
||||
RTTelemetryTableController,
|
||||
TelemetryTableController,
|
||||
RealtimeTableController,
|
||||
HistoricalTableController,
|
||||
TableOptionsController,
|
||||
Region,
|
||||
InspectorRegion,
|
||||
@@ -107,13 +107,13 @@ define([
|
||||
],
|
||||
"controllers": [
|
||||
{
|
||||
"key": "TelemetryTableController",
|
||||
"implementation": TelemetryTableController,
|
||||
"key": "HistoricalTableController",
|
||||
"implementation": HistoricalTableController,
|
||||
"depends": ["$scope", "telemetryHandler", "telemetryFormatter"]
|
||||
},
|
||||
{
|
||||
"key": "RTTelemetryTableController",
|
||||
"implementation": RTTelemetryTableController,
|
||||
"key": "RealtimeTableController",
|
||||
"implementation": RealtimeTableController,
|
||||
"depends": ["$scope", "telemetryHandler", "telemetryFormatter"]
|
||||
},
|
||||
{
|
||||
@@ -128,7 +128,7 @@ define([
|
||||
"name": "Historical Table",
|
||||
"key": "table",
|
||||
"glyph": "\ue604",
|
||||
"templateUrl": "templates/table.html",
|
||||
"templateUrl": "templates/historical-table.html",
|
||||
"needs": [
|
||||
"telemetry"
|
||||
],
|
||||
@@ -159,6 +159,12 @@ define([
|
||||
"key": "table-options-edit",
|
||||
"templateUrl": "templates/table-options-edit.html"
|
||||
}
|
||||
],
|
||||
"stylesheets": [
|
||||
{
|
||||
"stylesheetUrl": "css/table.css",
|
||||
"priority": "mandatory"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,28 +20,43 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
"./src/CachingPersistenceDecorator",
|
||||
'legacyRegistry'
|
||||
], function (
|
||||
CachingPersistenceDecorator,
|
||||
legacyRegistry
|
||||
) {
|
||||
.sizing-table {
|
||||
min-width: 100%;
|
||||
z-index: -1;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
|
||||
legacyRegistry.register("platform/persistence/cache", {
|
||||
"name": "Persistence cache",
|
||||
"description": "Cache to improve availability of persisted objects.",
|
||||
"extensions": {
|
||||
"components": [
|
||||
{
|
||||
"provides": "persistenceService",
|
||||
"type": "decorator",
|
||||
"implementation": CachingPersistenceDecorator,
|
||||
"depends": [
|
||||
"PERSISTENCE_SPACE"
|
||||
]
|
||||
}
|
||||
]
|
||||
//Add some padding to allow for decorations such as limits indicator
|
||||
td {
|
||||
padding-right: 15px;
|
||||
padding-left: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.mct-table {
|
||||
table-layout: fixed;
|
||||
thead {
|
||||
display: block;
|
||||
tr {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
th {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
tbody {
|
||||
tr {
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
td {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<div ng-controller="TelemetryTableController">
|
||||
<div ng-controller="HistoricalTableController">
|
||||
<mct-table
|
||||
headers="headers"
|
||||
rows="rows"
|
||||
@@ -1,22 +1,25 @@
|
||||
<div class="l-view-section scrolling"
|
||||
ng-style="overrideRowPositioning ?
|
||||
{'overflow': 'auto'} :
|
||||
{'overflow': 'scroll'}"
|
||||
>
|
||||
<table class="filterable"
|
||||
ng-style="overrideRowPositioning && {
|
||||
<div class="l-view-section scrolling" style="overflow: auto;">
|
||||
<table class="sizing-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td ng-repeat="header in displayHeaders">{{header}}</td>
|
||||
</tr>
|
||||
<tr><td ng-repeat="header in displayHeaders" >
|
||||
{{sizingRow[header].text}}
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="filterable mct-table"
|
||||
ng-style="{
|
||||
height: totalHeight + 'px',
|
||||
'table-layout': overrideRowPositioning ? 'fixed' : 'auto',
|
||||
'max-width': totalWidth
|
||||
}">
|
||||
}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-repeat="header in displayHeaders"
|
||||
ng-style="overrideRowPositioning && {
|
||||
ng-style="{
|
||||
width: columnWidths[$index] + 'px',
|
||||
'max-width': columnWidths[$index] + 'px',
|
||||
overflow: 'none',
|
||||
'box-sizing': 'border-box'
|
||||
}"
|
||||
ng-class="[
|
||||
enableSort ? 'sortable' : '',
|
||||
@@ -29,11 +32,9 @@
|
||||
</tr>
|
||||
<tr ng-if="enableFilter" class="s-filters">
|
||||
<th ng-repeat="header in displayHeaders"
|
||||
ng-style="overrideRowPositioning && {
|
||||
ng-style="{
|
||||
width: columnWidths[$index] + 'px',
|
||||
'max-width': columnWidths[$index] + 'px',
|
||||
overflow: 'none',
|
||||
'box-sizing': 'border-box'
|
||||
}">
|
||||
<input type="text"
|
||||
ng-model="filters[header]"/>
|
||||
@@ -41,21 +42,15 @@
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody ng-style="overrideRowPositioning ? '' : {
|
||||
'opacity': '0.0'
|
||||
}">
|
||||
<tbody>
|
||||
<tr ng-repeat="visibleRow in visibleRows track by visibleRow.rowIndex"
|
||||
ng-style="overrideRowPositioning && {
|
||||
position: 'absolute',
|
||||
ng-style="{
|
||||
top: visibleRow.offsetY + 'px',
|
||||
}">
|
||||
<td ng-repeat="header in displayHeaders"
|
||||
ng-style="overrideRowPositioning && {
|
||||
ng-style=" {
|
||||
width: columnWidths[$index] + 'px',
|
||||
'white-space': 'nowrap',
|
||||
'max-width': columnWidths[$index] + 'px',
|
||||
overflow: 'hidden',
|
||||
'box-sizing': 'border-box'
|
||||
}"
|
||||
class="{{visibleRow.contents[header].cssClass}}">
|
||||
{{ visibleRow.contents[header].text }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div ng-controller="RTTelemetryTableController">
|
||||
<div ng-controller="RealtimeTableController">
|
||||
<mct-table
|
||||
headers="headers"
|
||||
rows="rows"
|
||||
|
||||
@@ -45,7 +45,7 @@ define(
|
||||
* @param metadata Metadata describing the domains and ranges available
|
||||
* @returns {TableConfiguration} This object
|
||||
*/
|
||||
TableConfiguration.prototype.buildColumns = function (metadata) {
|
||||
TableConfiguration.prototype.populateColumns = function (metadata) {
|
||||
var self = this;
|
||||
|
||||
this.columns = [];
|
||||
@@ -138,8 +138,7 @@ define(
|
||||
* @private
|
||||
*/
|
||||
TableConfiguration.prototype.defaultColumnConfiguration = function () {
|
||||
return ((this.domainObject.getModel().configuration || {}).table ||
|
||||
{}).columns || {};
|
||||
return ((this.domainObject.getModel().configuration || {}).table || {}).columns || {};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -154,6 +153,16 @@ define(
|
||||
});
|
||||
};
|
||||
|
||||
function configChanged(config1, config2) {
|
||||
var config1Keys = Object.keys(config1),
|
||||
config2Keys = Object.keys(config2);
|
||||
|
||||
return (config1Keys.length !== config2Keys.length) ||
|
||||
config1Keys.some(function(key){
|
||||
return config1[key] !== config2[key];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* As part of the process of building the table definition, extract
|
||||
* configuration from column definitions.
|
||||
@@ -161,7 +170,7 @@ define(
|
||||
* pairs where the key is the column title, and the value is a
|
||||
* boolean indicating whether the column should be shown.
|
||||
*/
|
||||
TableConfiguration.prototype.getColumnConfiguration = function () {
|
||||
TableConfiguration.prototype.buildColumnConfiguration = function () {
|
||||
var configuration = {},
|
||||
//Use existing persisted config, or default it
|
||||
defaultConfig = this.defaultColumnConfiguration();
|
||||
@@ -177,6 +186,11 @@ define(
|
||||
defaultConfig[columnTitle];
|
||||
});
|
||||
|
||||
//Synchronize column configuration with model
|
||||
if (configChanged(configuration, defaultConfig)) {
|
||||
this.saveColumnConfiguration(configuration);
|
||||
}
|
||||
|
||||
return configuration;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[
|
||||
'./TelemetryTableController'
|
||||
],
|
||||
function (TableController) {
|
||||
|
||||
/**
|
||||
* Extends TelemetryTableController and adds real-time streaming
|
||||
* support.
|
||||
* @memberof platform/features/table
|
||||
* @param $scope
|
||||
* @param telemetryHandler
|
||||
* @param telemetryFormatter
|
||||
* @constructor
|
||||
*/
|
||||
function HistoricalTableController($scope, telemetryHandler, telemetryFormatter) {
|
||||
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
|
||||
}
|
||||
|
||||
HistoricalTableController.prototype = Object.create(TableController.prototype);
|
||||
|
||||
/**
|
||||
* Populates historical data on scope when it becomes available from
|
||||
* the telemetry API
|
||||
*/
|
||||
HistoricalTableController.prototype.addHistoricalData = function () {
|
||||
var rowData = [],
|
||||
self = this;
|
||||
|
||||
this.handle.getTelemetryObjects().forEach(function (telemetryObject){
|
||||
var series = self.handle.getSeries(telemetryObject) || {},
|
||||
pointCount = series.getPointCount ? series.getPointCount() : 0,
|
||||
i = 0;
|
||||
|
||||
for (; i < pointCount; i++) {
|
||||
rowData.push(self.table.getRowValues(telemetryObject,
|
||||
self.handle.makeDatum(telemetryObject, series, i)));
|
||||
}
|
||||
});
|
||||
|
||||
this.$scope.rows = rowData;
|
||||
};
|
||||
|
||||
return HistoricalTableController;
|
||||
}
|
||||
);
|
||||
@@ -21,10 +21,13 @@ define(
|
||||
this.maxDisplayRows = 50;
|
||||
|
||||
this.scrollable = element.find('div');
|
||||
this.thead = element.find('thead');
|
||||
this.tbody = element.find('tbody');
|
||||
this.$scope.sizingRow = {};
|
||||
|
||||
this.scrollable.on('scroll', this.onScroll.bind(this));
|
||||
|
||||
$scope.visibleRows = [];
|
||||
$scope.overrideRowPositioning = false;
|
||||
|
||||
/**
|
||||
* Set default values for optional parameters on a given scope
|
||||
@@ -56,22 +59,22 @@ define(
|
||||
$scope.sortColumn = undefined;
|
||||
$scope.sortDirection = undefined;
|
||||
}
|
||||
self.updateRows($scope.rows);
|
||||
self.setRows($scope.rows);
|
||||
};
|
||||
|
||||
/*
|
||||
* Define watches to listen for changes to headers and rows.
|
||||
*/
|
||||
$scope.$watchCollection('filters', function () {
|
||||
self.updateRows($scope.rows);
|
||||
self.setRows($scope.rows);
|
||||
});
|
||||
$scope.$watch('headers', this.updateHeaders.bind(this));
|
||||
$scope.$watch('rows', this.updateRows.bind(this));
|
||||
$scope.$watch('headers', this.setHeaders.bind(this));
|
||||
$scope.$watch('rows', this.setRows.bind(this));
|
||||
|
||||
/*
|
||||
* Listen for rows added individually (eg. for real-time tables)
|
||||
*/
|
||||
$scope.$on('add:row', this.newRow.bind(this));
|
||||
$scope.$on('add:row', this.addRow.bind(this));
|
||||
$scope.$on('remove:row', this.removeRow.bind(this));
|
||||
}
|
||||
|
||||
@@ -94,23 +97,27 @@ define(
|
||||
|
||||
/**
|
||||
* Handles a row add event. Rows can be added as needed using the
|
||||
* `addRow` broadcast event.
|
||||
* `add:row` broadcast event.
|
||||
* @private
|
||||
*/
|
||||
MCTTableController.prototype.newRow = function (event, rowIndex) {
|
||||
MCTTableController.prototype.addRow = function (event, rowIndex) {
|
||||
var row = this.$scope.rows[rowIndex];
|
||||
//Add row to the filtered, sorted list of all rows
|
||||
if (this.filterRows([row]).length > 0) {
|
||||
this.insertSorted(this.$scope.displayRows, row);
|
||||
}
|
||||
|
||||
this.$timeout(this.setElementSizes.bind(this))
|
||||
.then(this.scrollToBottom.bind(this));
|
||||
//Does the row pass the current filter?
|
||||
if (this.filterRows([row]).length === 1) {
|
||||
//Insert the row into the correct position in the array
|
||||
this.insertSorted(this.$scope.displayRows, row);
|
||||
|
||||
//Resize the columns , then update the rows visible in the table
|
||||
this.resize([this.$scope.sizingRow, row])
|
||||
.then(this.setVisibleRows.bind(this))
|
||||
.then(this.scrollToBottom.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles a row add event. Rows can be added as needed using the
|
||||
* `addRow` broadcast event.
|
||||
* Handles a row remove event. Rows can be removed as needed using the
|
||||
* `remove:row` broadcast event.
|
||||
* @private
|
||||
*/
|
||||
MCTTableController.prototype.removeRow = function (event, rowIndex) {
|
||||
@@ -223,7 +230,7 @@ define(
|
||||
* enabled, reset filters. If sorting is enabled, reset
|
||||
* sorting.
|
||||
*/
|
||||
MCTTableController.prototype.updateHeaders = function (newHeaders) {
|
||||
MCTTableController.prototype.setHeaders = function (newHeaders) {
|
||||
if (!newHeaders){
|
||||
return;
|
||||
}
|
||||
@@ -239,7 +246,7 @@ define(
|
||||
this.$scope.sortColumn = undefined;
|
||||
this.$scope.sortDirection = undefined;
|
||||
}
|
||||
this.updateRows(this.$scope.rows);
|
||||
this.setRows(this.$scope.rows);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -247,12 +254,12 @@ define(
|
||||
* for individual rows.
|
||||
*/
|
||||
MCTTableController.prototype.setElementSizes = function () {
|
||||
var thead = this.element.find('thead'),
|
||||
tbody = this.element.find('tbody'),
|
||||
var thead = this.thead,
|
||||
tbody = this.tbody,
|
||||
firstRow = tbody.find('tr'),
|
||||
column = firstRow.find('td'),
|
||||
headerHeight = thead.prop('offsetHeight'),
|
||||
rowHeight = 20,
|
||||
rowHeight = firstRow.prop('offsetHeight'),
|
||||
columnWidth,
|
||||
tableWidth = 0,
|
||||
overallHeight = headerHeight + (rowHeight *
|
||||
@@ -269,15 +276,12 @@ define(
|
||||
this.$scope.headerHeight = headerHeight;
|
||||
this.$scope.rowHeight = rowHeight;
|
||||
this.$scope.totalHeight = overallHeight;
|
||||
this.setVisibleRows();
|
||||
|
||||
if (tableWidth > 0) {
|
||||
this.$scope.totalWidth = tableWidth + 'px';
|
||||
} else {
|
||||
this.$scope.totalWidth = 'none';
|
||||
}
|
||||
|
||||
this.$scope.overrideRowPositioning = true;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -289,21 +293,14 @@ define(
|
||||
sortKey = this.$scope.sortColumn;
|
||||
|
||||
function binarySearch(searchArray, searchElement, min, max){
|
||||
var sampleAt = Math.floor((max - min) / 2) + min,
|
||||
valA,
|
||||
valB;
|
||||
var sampleAt = Math.floor((max - min) / 2) + min;
|
||||
|
||||
if (max < min) {
|
||||
return min; // Element is not in array, min gives direction
|
||||
}
|
||||
|
||||
valA = isNaN(searchElement[sortKey].text) ?
|
||||
searchElement[sortKey].text :
|
||||
parseFloat(searchElement[sortKey].text);
|
||||
valB = isNaN(searchArray[sampleAt][sortKey].text) ?
|
||||
searchArray[sampleAt][sortKey].text :
|
||||
parseFloat(searchArray[sampleAt][sortKey].text);
|
||||
|
||||
switch(self.sortComparator(valA, valB)) {
|
||||
switch(self.sortComparator(searchElement[sortKey].text,
|
||||
searchArray[sampleAt][sortKey].text)) {
|
||||
case -1:
|
||||
return binarySearch(searchArray, searchElement, min,
|
||||
sampleAt - 1);
|
||||
@@ -341,8 +338,34 @@ define(
|
||||
*/
|
||||
MCTTableController.prototype.sortComparator = function (a, b) {
|
||||
var result = 0,
|
||||
sortDirectionMultiplier;
|
||||
sortDirectionMultiplier,
|
||||
numberA,
|
||||
numberB;
|
||||
/**
|
||||
* Given a value, if it is a number, or a string representation of a
|
||||
* number, then return a number representation. Otherwise, return
|
||||
* the original value. It's a little more robust than using just
|
||||
* Number() or parseFloat, or isNaN in isolation, all of which are
|
||||
* fairly inconsistent in their results.
|
||||
* @param value The value to return as a number.
|
||||
* @returns {*} The value cast to a Number, or the original value if
|
||||
* a Number representation is not possible.
|
||||
*/
|
||||
function toNumber (value){
|
||||
var val = !isNaN(Number(value)) && !isNaN(parseFloat(value)) ? Number(value) : value;
|
||||
return val;
|
||||
}
|
||||
|
||||
numberA = toNumber(a);
|
||||
numberB = toNumber(b);
|
||||
|
||||
//If they're both numbers, then compare them as numbers
|
||||
if (typeof numberA === "number" && typeof numberB === "number") {
|
||||
a = numberA;
|
||||
b = numberB;
|
||||
}
|
||||
|
||||
//If they're both strings, then ignore case
|
||||
if (typeof a === "string" && typeof b === "string") {
|
||||
a = a.toLowerCase();
|
||||
b = b.toLowerCase();
|
||||
@@ -379,15 +402,7 @@ define(
|
||||
}
|
||||
|
||||
return rowsToSort.sort(function (a, b) {
|
||||
//If the values to compare can be compared as
|
||||
// numbers, do so. String comparison of number
|
||||
// values can cause inconsistencies
|
||||
var valA = isNaN(a[sortKey].text) ? a[sortKey].text :
|
||||
parseFloat(a[sortKey].text),
|
||||
valB = isNaN(b[sortKey].text) ? b[sortKey].text :
|
||||
parseFloat(b[sortKey].text);
|
||||
|
||||
return self.sortComparator(valA, valB);
|
||||
return self.sortComparator(a[sortKey].text, b[sortKey].text);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -397,74 +412,48 @@ define(
|
||||
* pre-calculate optimal column sizes without having to render
|
||||
* every row.
|
||||
*/
|
||||
MCTTableController.prototype.findLargestRow = function (rows) {
|
||||
var largestRow = rows.reduce(function (largestRow, row) {
|
||||
MCTTableController.prototype.buildLargestRow = function (rows) {
|
||||
var largestRow = rows.reduce(function (prevLargest, row) {
|
||||
Object.keys(row).forEach(function (key) {
|
||||
var currentColumn = row[key].text,
|
||||
var currentColumn,
|
||||
currentColumnLength,
|
||||
largestColumn,
|
||||
largestColumnLength;
|
||||
if (row[key]){
|
||||
currentColumn = (row[key]).text;
|
||||
currentColumnLength =
|
||||
(currentColumn && currentColumn.length) ?
|
||||
currentColumn.length :
|
||||
currentColumn,
|
||||
largestColumn = largestRow[key].text,
|
||||
largestColumnLength =
|
||||
(largestColumn && largestColumn.length) ?
|
||||
largestColumn.length :
|
||||
largestColumn;
|
||||
currentColumn;
|
||||
largestColumn = prevLargest[key] ? prevLargest[key].text : "";
|
||||
largestColumnLength = largestColumn.length;
|
||||
|
||||
if (currentColumnLength > largestColumnLength) {
|
||||
largestRow[key] = JSON.parse(JSON.stringify(row[key]));
|
||||
if (currentColumnLength > largestColumnLength) {
|
||||
prevLargest[key] = JSON.parse(JSON.stringify(row[key]));
|
||||
}
|
||||
}
|
||||
});
|
||||
return largestRow;
|
||||
return prevLargest;
|
||||
}, JSON.parse(JSON.stringify(rows[0] || {})));
|
||||
|
||||
largestRow = JSON.parse(JSON.stringify(largestRow));
|
||||
|
||||
// Pad with characters to accomodate variable-width fonts,
|
||||
// and remove characters that would allow word-wrapping.
|
||||
Object.keys(largestRow).forEach(function (key) {
|
||||
var padCharacters,
|
||||
i;
|
||||
|
||||
largestRow[key].text = String(largestRow[key].text);
|
||||
padCharacters = largestRow[key].text.length / 10;
|
||||
for (i = 0; i < padCharacters; i++) {
|
||||
largestRow[key].text = largestRow[key].text + 'W';
|
||||
}
|
||||
largestRow[key].text = largestRow[key].text
|
||||
.replace(/[ \-_]/g, 'W');
|
||||
});
|
||||
return largestRow;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the widest row in the table, pads that row, and adds
|
||||
* it to the table. Allows the table to size itself, then uses this
|
||||
* as basis for column dimensions.
|
||||
* Calculates the widest row in the table, and if necessary, resizes
|
||||
* the table accordingly
|
||||
*
|
||||
* @param rows the rows on which to resize
|
||||
* @returns {Promise} a promise that will resolve when resizing has
|
||||
* occurred.
|
||||
* @private
|
||||
*/
|
||||
MCTTableController.prototype.resize = function (){
|
||||
var largestRow = this.findLargestRow(this.$scope.displayRows),
|
||||
self = this;
|
||||
this.$scope.visibleRows = [
|
||||
{
|
||||
rowIndex: 0,
|
||||
offsetY: undefined,
|
||||
contents: largestRow
|
||||
}
|
||||
];
|
||||
|
||||
//Wait a timeout to allow digest of previous change to visible
|
||||
// rows to happen.
|
||||
this.$timeout(function () {
|
||||
//Remove temporary padding row used for setting column widths
|
||||
self.$scope.visibleRows = [];
|
||||
self.setElementSizes();
|
||||
});
|
||||
MCTTableController.prototype.resize = function (rows) {
|
||||
this.$scope.sizingRow = this.buildLargestRow(rows);
|
||||
return this.$timeout(this.setElementSizes.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
* @priate
|
||||
* @private
|
||||
*/
|
||||
MCTTableController.prototype.filterAndSort = function (rows) {
|
||||
var displayRows = rows;
|
||||
@@ -475,26 +464,21 @@ define(
|
||||
if (this.$scope.enableSort) {
|
||||
displayRows = this.sortRows(displayRows.slice(0));
|
||||
}
|
||||
this.$scope.displayRows = displayRows;
|
||||
return displayRows;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update rows with new data. If filtering is enabled, rows
|
||||
* will be sorted before display.
|
||||
*/
|
||||
MCTTableController.prototype.updateRows = function (newRows) {
|
||||
//Reset visible rows because new row data available.
|
||||
this.$scope.visibleRows = [];
|
||||
|
||||
this.$scope.overrideRowPositioning = false;
|
||||
|
||||
MCTTableController.prototype.setRows = function (newRows) {
|
||||
//Nothing to show because no columns visible
|
||||
if (!this.$scope.displayHeaders) {
|
||||
if (!this.$scope.displayHeaders || !newRows) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.filterAndSort(newRows || []);
|
||||
this.resize();
|
||||
this.$scope.displayRows = this.filterAndSort(newRows || []);
|
||||
this.resize(newRows).then(this.setVisibleRows.bind(this));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,7 +35,7 @@ define(
|
||||
* @param telemetryFormatter
|
||||
* @constructor
|
||||
*/
|
||||
function RTTelemetryTableController($scope, telemetryHandler, telemetryFormatter) {
|
||||
function RealtimeTableController($scope, telemetryHandler, telemetryFormatter) {
|
||||
TableController.call(this, $scope, telemetryHandler, telemetryFormatter);
|
||||
|
||||
$scope.autoScroll = false;
|
||||
@@ -64,58 +64,35 @@ define(
|
||||
});
|
||||
}
|
||||
|
||||
RTTelemetryTableController.prototype = Object.create(TableController.prototype);
|
||||
RealtimeTableController.prototype = Object.create(TableController.prototype);
|
||||
|
||||
/**
|
||||
Override the subscribe function defined on the parent controller in
|
||||
order to handle realtime telemetry instead of historical.
|
||||
* Overrides method on TelemetryTableController providing handling
|
||||
* for realtime data.
|
||||
*/
|
||||
RTTelemetryTableController.prototype.subscribe = function () {
|
||||
var self = this;
|
||||
self.$scope.rows = undefined;
|
||||
(this.subscriptions || []).forEach(function (unsubscribe){
|
||||
unsubscribe();
|
||||
});
|
||||
RealtimeTableController.prototype.addRealtimeData = function() {
|
||||
var self = this,
|
||||
datum,
|
||||
row;
|
||||
this.handle.getTelemetryObjects().forEach(function (telemetryObject){
|
||||
datum = self.handle.getDatum(telemetryObject);
|
||||
if (datum) {
|
||||
//Populate row values from telemetry datum
|
||||
row = self.table.getRowValues(telemetryObject, datum);
|
||||
self.$scope.rows.push(row);
|
||||
|
||||
if (this.handle) {
|
||||
this.handle.unsubscribe();
|
||||
}
|
||||
|
||||
function updateData(){
|
||||
var datum,
|
||||
row;
|
||||
self.handle.getTelemetryObjects().forEach(function (telemetryObject){
|
||||
datum = self.handle.getDatum(telemetryObject);
|
||||
if (datum) {
|
||||
row = self.table.getRowValues(telemetryObject, datum);
|
||||
if (!self.$scope.rows){
|
||||
self.$scope.rows = [row];
|
||||
self.$scope.$digest();
|
||||
} else {
|
||||
self.$scope.rows.push(row);
|
||||
|
||||
if (self.$scope.rows.length > self.maxRows) {
|
||||
self.$scope.$broadcast('remove:row', 0);
|
||||
self.$scope.rows.shift();
|
||||
}
|
||||
|
||||
self.$scope.$broadcast('add:row',
|
||||
self.$scope.rows.length - 1);
|
||||
}
|
||||
//Inform table that a new row has been added
|
||||
if (self.$scope.rows.length > self.maxRows) {
|
||||
self.$scope.$broadcast('remove:row', 0);
|
||||
self.$scope.rows.shift();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
|
||||
this.$scope.domainObject,
|
||||
updateData,
|
||||
true // Lossless
|
||||
);
|
||||
|
||||
this.setup();
|
||||
self.$scope.$broadcast('add:row',
|
||||
self.$scope.rows.length - 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return RTTelemetryTableController;
|
||||
return RealtimeTableController;
|
||||
}
|
||||
);
|
||||
@@ -49,13 +49,29 @@ define(
|
||||
|
||||
this.$scope = $scope;
|
||||
this.domainObject = $scope.domainObject;
|
||||
this.listeners = [];
|
||||
|
||||
$scope.columnsForm = {};
|
||||
|
||||
this.domainObject.getCapability('mutation').listen(function (model) {
|
||||
self.populateForm(model);
|
||||
function unlisten() {
|
||||
self.listeners.forEach(function (listener) {
|
||||
listener();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$watch('domainObject', function(domainObject) {
|
||||
unlisten();
|
||||
self.populateForm(domainObject.getModel());
|
||||
|
||||
self.listeners.push(self.domainObject.getCapability('mutation').listen(function (model) {
|
||||
self.populateForm(model);
|
||||
}));
|
||||
});
|
||||
|
||||
/**
|
||||
* Maintain a configuration object on scope that stores column
|
||||
* configuration. On change, synchronize with object model.
|
||||
*/
|
||||
$scope.$watchCollection('configuration.table.columns', function (columns){
|
||||
if (columns){
|
||||
self.domainObject.useCapability('mutation', function (model) {
|
||||
@@ -65,6 +81,11 @@ define(
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Destroy all mutation listeners
|
||||
*/
|
||||
$scope.$on('$destroy', unlisten);
|
||||
|
||||
}
|
||||
|
||||
TableOptionsController.prototype.populateForm = function (model) {
|
||||
@@ -84,7 +105,7 @@ define(
|
||||
'key': key
|
||||
});
|
||||
});
|
||||
this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration));
|
||||
this.$scope.configuration = JSON.parse(JSON.stringify(model.configuration || {}));
|
||||
};
|
||||
|
||||
return TableOptionsController;
|
||||
|
||||
@@ -50,20 +50,15 @@ define(
|
||||
this.$scope = $scope;
|
||||
this.columns = {}; //Range and Domain columns
|
||||
this.handle = undefined;
|
||||
//this.pending = false;
|
||||
this.telemetryHandler = telemetryHandler;
|
||||
this.table = new TableConfiguration($scope.domainObject,
|
||||
telemetryFormatter);
|
||||
this.changeListeners = [];
|
||||
|
||||
$scope.rows = undefined;
|
||||
$scope.rows = [];
|
||||
|
||||
// Subscribe to telemetry when a domain object becomes available
|
||||
this.$scope.$watch('domainObject', function(domainObject){
|
||||
if (!domainObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$scope.$watch('domainObject', function(){
|
||||
self.subscribe();
|
||||
self.registerChangeListeners();
|
||||
});
|
||||
@@ -72,16 +67,24 @@ define(
|
||||
this.$scope.$on("$destroy", this.destroy.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
TelemetryTableController.prototype.unregisterChangeListeners = function () {
|
||||
this.changeListeners.forEach(function (listener) {
|
||||
return listener && listener();
|
||||
});
|
||||
this.changeListeners = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Defer registration of change listeners until domain object is
|
||||
* available in order to avoid race conditions
|
||||
* @private
|
||||
*/
|
||||
TelemetryTableController.prototype.registerChangeListeners = function () {
|
||||
this.changeListeners.forEach(function (listener) {
|
||||
return listener && listener();
|
||||
});
|
||||
this.changeListeners = [];
|
||||
this.unregisterChangeListeners();
|
||||
|
||||
// When composition changes, re-subscribe to the various
|
||||
// telemetry subscriptions
|
||||
this.changeListeners.push(this.$scope.$watchCollection(
|
||||
@@ -102,6 +105,24 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function for handling realtime data when it is available. This
|
||||
* will be called by the telemetry framework when new data is
|
||||
* available.
|
||||
*
|
||||
* Method should be overridden by specializing class.
|
||||
*/
|
||||
TelemetryTableController.prototype.addRealtimeData = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* Function for handling historical data. Will be called by
|
||||
* telemetry framework when requested historical data is available.
|
||||
* Should be overridden by specializing class.
|
||||
*/
|
||||
TelemetryTableController.prototype.addHistoricalData = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
Create a new subscription. This can be overridden by children to
|
||||
change default behaviour (which is to retrieve historical telemetry
|
||||
@@ -112,13 +133,9 @@ define(
|
||||
this.handle.unsubscribe();
|
||||
}
|
||||
|
||||
//Noop because not supporting realtime data right now
|
||||
function noop(){
|
||||
}
|
||||
|
||||
this.handle = this.$scope.domainObject && this.telemetryHandler.handle(
|
||||
this.$scope.domainObject,
|
||||
noop,
|
||||
this.addRealtimeData.bind(this),
|
||||
true // Lossless
|
||||
);
|
||||
|
||||
@@ -127,28 +144,6 @@ define(
|
||||
this.setup();
|
||||
};
|
||||
|
||||
/**
|
||||
* Populates historical data on scope when it becomes available
|
||||
* @private
|
||||
*/
|
||||
TelemetryTableController.prototype.addHistoricalData = function () {
|
||||
var rowData = [],
|
||||
self = this;
|
||||
|
||||
this.handle.getTelemetryObjects().forEach(function (telemetryObject){
|
||||
var series = self.handle.getSeries(telemetryObject) || {},
|
||||
pointCount = series.getPointCount ? series.getPointCount() : 0,
|
||||
i = 0;
|
||||
|
||||
for (; i < pointCount; i++) {
|
||||
rowData.push(self.table.getRowValues(telemetryObject,
|
||||
self.handle.makeDatum(telemetryObject, series, i)));
|
||||
}
|
||||
});
|
||||
|
||||
this.$scope.rows = rowData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup table columns based on domain object metadata
|
||||
*/
|
||||
@@ -159,7 +154,9 @@ define(
|
||||
|
||||
if (handle) {
|
||||
handle.promiseTelemetryObjects().then(function () {
|
||||
table.buildColumns(handle.getMetadata());
|
||||
self.$scope.headers = [];
|
||||
self.$scope.rows = [];
|
||||
table.populateColumns(handle.getMetadata());
|
||||
|
||||
self.filterColumns();
|
||||
|
||||
@@ -173,26 +170,14 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param object The object for which data is available (table may
|
||||
* be composed of multiple objects)
|
||||
* @param datum The data received from the telemetry source
|
||||
*/
|
||||
TelemetryTableController.prototype.updateRows = function (object, datum) {
|
||||
this.$scope.rows.push(this.table.getRowValues(object, datum));
|
||||
};
|
||||
|
||||
/**
|
||||
* When column configuration changes, update the visible headers
|
||||
* accordingly.
|
||||
* @private
|
||||
*/
|
||||
TelemetryTableController.prototype.filterColumns = function (columnConfig) {
|
||||
if (!columnConfig){
|
||||
columnConfig = this.table.getColumnConfiguration();
|
||||
this.table.saveColumnConfiguration(columnConfig);
|
||||
}
|
||||
TelemetryTableController.prototype.filterColumns = function () {
|
||||
var columnConfig = this.table.buildColumnConfiguration();
|
||||
|
||||
//Populate headers with visible columns (determined by configuration)
|
||||
this.$scope.headers = Object.keys(columnConfig).filter(function (column) {
|
||||
return columnConfig[column];
|
||||
|
||||
@@ -30,6 +30,51 @@ define(
|
||||
* Defines a generic 'Table' component. The table can be populated
|
||||
* en-masse by setting the rows attribute, or rows can be added as
|
||||
* needed via a broadcast 'addRow' event.
|
||||
*
|
||||
* This directive accepts parameters specifying header and row
|
||||
* content, as well as some additional options.
|
||||
*
|
||||
* Two broadcast events for notifying the table that the rows have
|
||||
* changed. For performance reasons, the table does not monitor the
|
||||
* content of `rows` constantly.
|
||||
* - 'add:row': A $broadcast event that will notify the table that
|
||||
* a new row has been added to the table.
|
||||
* eg.
|
||||
* <pre><code>
|
||||
* $scope.rows.push(newRow);
|
||||
* $scope.$broadcast('add:row', $scope.rows.length-1);
|
||||
* </code></pre>
|
||||
* The code above adds a new row, and alerts the table using the
|
||||
* add:row event. Sorting and filtering will be applied
|
||||
* automatically by the table component.
|
||||
*
|
||||
* - 'remove:row': A $broadcast event that will notify the table that a
|
||||
* row should be removed from the table.
|
||||
* eg.
|
||||
* <pre><code>
|
||||
* $scope.rows.slice(5, 1);
|
||||
* $scope.$broadcast('remove:row', 5);
|
||||
* </code></pre>
|
||||
* The code above removes a row from the rows array, and then alerts
|
||||
* the table to its removal.
|
||||
*
|
||||
* @memberof platform/features/table
|
||||
* @param {string[]} headers The column titles to appear at the top
|
||||
* of the table. Corresponding values are specified in the rows
|
||||
* using the header title provided here.
|
||||
* @param {Object[]} rows The row content. Each row is an object
|
||||
* with key-value pairs where the key corresponds to a header
|
||||
* specified in the headers parameter.
|
||||
* @param {boolean} enableFilter If true, values will be searchable
|
||||
* and results filtered
|
||||
* @param {boolean} enableSort If true, sorting will be enabled
|
||||
* allowing sorting by clicking on column headers
|
||||
* @param {boolean} autoScroll If true, table will automatically
|
||||
* scroll to the bottom as new data arrives. Auto-scroll can be
|
||||
* disengaged manually by scrolling away from the bottom of the
|
||||
* table, and can also be enabled manually by scrolling to the bottom of
|
||||
* the table rows.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function MCTTable($timeout) {
|
||||
|
||||
@@ -114,10 +114,10 @@ define(
|
||||
}];
|
||||
|
||||
beforeEach(function() {
|
||||
table.buildColumns(metadata);
|
||||
table.populateColumns(metadata);
|
||||
});
|
||||
|
||||
it("populates the columns attribute", function() {
|
||||
it("populates columns", function() {
|
||||
expect(table.columns.length).toBe(5);
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ define(
|
||||
|
||||
it("Provides a default configuration with all columns" +
|
||||
" visible", function() {
|
||||
var configuration = table.getColumnConfiguration();
|
||||
var configuration = table.buildColumnConfiguration();
|
||||
|
||||
expect(configuration).toBeDefined();
|
||||
expect(Object.keys(configuration).every(function(key){
|
||||
@@ -158,7 +158,7 @@ define(
|
||||
};
|
||||
mockModel.configuration = modelConfig;
|
||||
|
||||
tableConfig = table.getColumnConfiguration();
|
||||
tableConfig = table.buildColumnConfiguration();
|
||||
|
||||
expect(tableConfig).toBeDefined();
|
||||
expect(tableConfig['Range 1']).toBe(false);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
define(
|
||||
[
|
||||
"../../src/controllers/TelemetryTableController"
|
||||
"../../src/controllers/HistoricalTableController"
|
||||
],
|
||||
function (TableController) {
|
||||
|
||||
@@ -71,14 +71,14 @@ define(
|
||||
|
||||
mockTable = jasmine.createSpyObj('table',
|
||||
[
|
||||
'buildColumns',
|
||||
'getColumnConfiguration',
|
||||
'populateColumns',
|
||||
'buildColumnConfiguration',
|
||||
'getRowValues',
|
||||
'saveColumnConfiguration'
|
||||
]
|
||||
);
|
||||
mockTable.columns = [];
|
||||
mockTable.getColumnConfiguration.andReturn(mockConfiguration);
|
||||
mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
|
||||
|
||||
mockDomainObject= jasmine.createSpyObj('domainObject', [
|
||||
'getCapability',
|
||||
@@ -124,19 +124,18 @@ define(
|
||||
expect(mockTelemetryHandle.unsubscribe).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('the controller makes use of the table', function () {
|
||||
describe('makes use of the table', function () {
|
||||
|
||||
it('to create column definitions from telemetry' +
|
||||
' metadata', function () {
|
||||
controller.setup();
|
||||
expect(mockTable.buildColumns).toHaveBeenCalled();
|
||||
expect(mockTable.populateColumns).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('to create column configuration, which is written to the' +
|
||||
' object model', function () {
|
||||
controller.setup();
|
||||
expect(mockTable.getColumnConfiguration).toHaveBeenCalled();
|
||||
expect(mockTable.saveColumnConfiguration).toHaveBeenCalled();
|
||||
expect(mockTable.buildColumnConfiguration).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,15 +56,18 @@ define(
|
||||
|
||||
mockElement = jasmine.createSpyObj('element', [
|
||||
'find',
|
||||
'prop',
|
||||
'on'
|
||||
]);
|
||||
mockElement.find.andReturn(mockElement);
|
||||
mockElement.prop.andReturn(0);
|
||||
|
||||
mockScope.displayHeaders = true;
|
||||
mockTimeout = jasmine.createSpy('$timeout');
|
||||
mockTimeout.andReturn(promise(undefined));
|
||||
|
||||
controller = new MCTTableController(mockScope, mockTimeout, mockElement);
|
||||
spyOn(controller, 'setVisibleRows');
|
||||
});
|
||||
|
||||
it('Reacts to changes to filters, headers, and rows', function() {
|
||||
@@ -113,7 +116,7 @@ define(
|
||||
});
|
||||
|
||||
it('Sets rows on scope when rows change', function() {
|
||||
controller.updateRows(testRows);
|
||||
controller.setRows(testRows);
|
||||
expect(mockScope.displayRows.length).toBe(3);
|
||||
expect(mockScope.displayRows).toEqual(testRows);
|
||||
});
|
||||
@@ -125,7 +128,7 @@ define(
|
||||
'col2': {'text': 'ghi'},
|
||||
'col3': {'text': 'row3 col3'}
|
||||
};
|
||||
controller.updateRows(testRows);
|
||||
controller.setRows(testRows);
|
||||
expect(mockScope.displayRows.length).toBe(3);
|
||||
testRows.push(row4);
|
||||
addRowFunc(undefined, 3);
|
||||
@@ -134,10 +137,8 @@ define(
|
||||
|
||||
it('Supports removing rows individually', function() {
|
||||
var removeRowFunc = mockScope.$on.calls[mockScope.$on.calls.length-1].args[1];
|
||||
controller.updateRows(testRows);
|
||||
controller.setRows(testRows);
|
||||
expect(mockScope.displayRows.length).toBe(3);
|
||||
spyOn(controller, 'setVisibleRows');
|
||||
//controller.setVisibleRows.andReturn(undefined);
|
||||
removeRowFunc(undefined, 2);
|
||||
expect(mockScope.displayRows.length).toBe(2);
|
||||
expect(controller.setVisibleRows).toHaveBeenCalled();
|
||||
@@ -177,7 +178,54 @@ define(
|
||||
expect(sortedRows[2].col2.text).toEqual('abc');
|
||||
});
|
||||
|
||||
describe('Adding new rows', function() {
|
||||
it('correctly sorts rows of differing types', function () {
|
||||
mockScope.sortColumn = 'col2';
|
||||
mockScope.sortDirection = 'desc';
|
||||
|
||||
testRows.push({
|
||||
'col1': {'text': 'row4 col1'},
|
||||
'col2': {'text': '123'},
|
||||
'col3': {'text': 'row4 col3'}
|
||||
});
|
||||
testRows.push({
|
||||
'col1': {'text': 'row5 col1'},
|
||||
'col2': {'text': '456'},
|
||||
'col3': {'text': 'row5 col3'}
|
||||
});
|
||||
testRows.push({
|
||||
'col1': {'text': 'row5 col1'},
|
||||
'col2': {'text': ''},
|
||||
'col3': {'text': 'row5 col3'}
|
||||
});
|
||||
|
||||
sortedRows = controller.sortRows(testRows);
|
||||
expect(sortedRows[0].col2.text).toEqual('ghi');
|
||||
expect(sortedRows[1].col2.text).toEqual('def');
|
||||
expect(sortedRows[2].col2.text).toEqual('abc');
|
||||
|
||||
expect(sortedRows[sortedRows.length-3].col2.text).toEqual('456');
|
||||
expect(sortedRows[sortedRows.length-2].col2.text).toEqual('123');
|
||||
expect(sortedRows[sortedRows.length-1].col2.text).toEqual('');
|
||||
});
|
||||
|
||||
describe('The sort comparator', function () {
|
||||
it('Correctly sorts different data types', function () {
|
||||
var val1 = "",
|
||||
val2 = "1",
|
||||
val3 = "2016-04-05 18:41:30.713Z",
|
||||
val4 = "1.1",
|
||||
val5 = "8.945520958175627e-13";
|
||||
mockScope.sortDirection = "asc";
|
||||
|
||||
expect(controller.sortComparator(val1, val2)).toEqual(-1);
|
||||
expect(controller.sortComparator(val3, val1)).toEqual(1);
|
||||
expect(controller.sortComparator(val3, val2)).toEqual(1);
|
||||
expect(controller.sortComparator(val4, val2)).toEqual(1);
|
||||
expect(controller.sortComparator(val2, val5)).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Adding new rows', function () {
|
||||
var row4,
|
||||
row5,
|
||||
row6;
|
||||
@@ -208,20 +256,20 @@ define(
|
||||
mockScope.displayRows = controller.sortRows(testRows.slice(0));
|
||||
|
||||
mockScope.rows.push(row4);
|
||||
controller.newRow(undefined, mockScope.rows.length-1);
|
||||
controller.addRow(undefined, mockScope.rows.length-1);
|
||||
expect(mockScope.displayRows[0].col2.text).toEqual('xyz');
|
||||
|
||||
mockScope.rows.push(row5);
|
||||
controller.newRow(undefined, mockScope.rows.length-1);
|
||||
controller.addRow(undefined, mockScope.rows.length-1);
|
||||
expect(mockScope.displayRows[4].col2.text).toEqual('aaa');
|
||||
|
||||
mockScope.rows.push(row6);
|
||||
controller.newRow(undefined, mockScope.rows.length-1);
|
||||
controller.addRow(undefined, mockScope.rows.length-1);
|
||||
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
|
||||
|
||||
//Add a duplicate row
|
||||
mockScope.rows.push(row6);
|
||||
controller.newRow(undefined, mockScope.rows.length-1);
|
||||
controller.addRow(undefined, mockScope.rows.length-1);
|
||||
expect(mockScope.displayRows[2].col2.text).toEqual('ggg');
|
||||
expect(mockScope.displayRows[3].col2.text).toEqual('ggg');
|
||||
});
|
||||
@@ -237,18 +285,18 @@ define(
|
||||
mockScope.displayRows = controller.filterRows(testRows);
|
||||
|
||||
mockScope.rows.push(row5);
|
||||
controller.newRow(undefined, mockScope.rows.length-1);
|
||||
controller.addRow(undefined, mockScope.rows.length-1);
|
||||
expect(mockScope.displayRows.length).toBe(2);
|
||||
expect(mockScope.displayRows[1].col2.text).toEqual('aaa');
|
||||
|
||||
mockScope.rows.push(row6);
|
||||
controller.newRow(undefined, mockScope.rows.length-1);
|
||||
controller.addRow(undefined, mockScope.rows.length-1);
|
||||
expect(mockScope.displayRows.length).toBe(2);
|
||||
//Row was not added because does not match filter
|
||||
});
|
||||
|
||||
it('Adds new rows at the correct sort position when' +
|
||||
' not sorted ', function() {
|
||||
' not sorted ', function () {
|
||||
mockScope.sortColumn = undefined;
|
||||
mockScope.sortDirection = undefined;
|
||||
mockScope.filters = {};
|
||||
@@ -256,14 +304,33 @@ define(
|
||||
mockScope.displayRows = testRows.slice(0);
|
||||
|
||||
mockScope.rows.push(row5);
|
||||
controller.newRow(undefined, mockScope.rows.length-1);
|
||||
controller.addRow(undefined, mockScope.rows.length-1);
|
||||
expect(mockScope.displayRows[3].col2.text).toEqual('aaa');
|
||||
|
||||
mockScope.rows.push(row6);
|
||||
controller.newRow(undefined, mockScope.rows.length-1);
|
||||
controller.addRow(undefined, mockScope.rows.length-1);
|
||||
expect(mockScope.displayRows[4].col2.text).toEqual('ggg');
|
||||
});
|
||||
|
||||
it('Resizes columns if length of any columns in new' +
|
||||
' row exceeds corresponding existing column', function() {
|
||||
var row7 = {
|
||||
'col1': {'text': 'row6 col1'},
|
||||
'col2': {'text': 'some longer string'},
|
||||
'col3': {'text': 'row6 col3'}
|
||||
};
|
||||
|
||||
mockScope.sortColumn = undefined;
|
||||
mockScope.sortDirection = undefined;
|
||||
mockScope.filters = {};
|
||||
|
||||
mockScope.displayRows = testRows.slice(0);
|
||||
|
||||
mockScope.rows.push(row7);
|
||||
controller.addRow(undefined, mockScope.rows.length-1);
|
||||
expect(controller.$scope.sizingRow.col2).toEqual({text: 'some longer string'});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
define(
|
||||
[
|
||||
"../../src/controllers/RTTelemetryTableController"
|
||||
"../../src/controllers/RealtimeTableController"
|
||||
],
|
||||
function (TableController) {
|
||||
|
||||
@@ -75,14 +75,14 @@ define(
|
||||
|
||||
mockTable = jasmine.createSpyObj('table',
|
||||
[
|
||||
'buildColumns',
|
||||
'getColumnConfiguration',
|
||||
'populateColumns',
|
||||
'buildColumnConfiguration',
|
||||
'getRowValues',
|
||||
'saveColumnConfiguration'
|
||||
]
|
||||
);
|
||||
mockTable.columns = [];
|
||||
mockTable.getColumnConfiguration.andReturn(mockConfiguration);
|
||||
mockTable.buildColumnConfiguration.andReturn(mockConfiguration);
|
||||
mockTable.getRowValues.andReturn(mockTableRow);
|
||||
|
||||
mockDomainObject= jasmine.createSpyObj('domainObject', [
|
||||
@@ -105,13 +105,16 @@ define(
|
||||
'unsubscribe',
|
||||
'getDatum',
|
||||
'promiseTelemetryObjects',
|
||||
'getTelemetryObjects'
|
||||
'getTelemetryObjects',
|
||||
'request'
|
||||
]);
|
||||
|
||||
// Arbitrary array with non-zero length, contents are not
|
||||
// used by mocks
|
||||
mockTelemetryHandle.getTelemetryObjects.andReturn([{}]);
|
||||
mockTelemetryHandle.promiseTelemetryObjects.andReturn(promise(undefined));
|
||||
mockTelemetryHandle.getDatum.andReturn({});
|
||||
mockTelemetryHandle.request.andReturn(promise(undefined));
|
||||
|
||||
mockTelemetryHandler = jasmine.createSpyObj('telemetryHandler', [
|
||||
'handle'
|
||||
@@ -37,18 +37,36 @@ define(
|
||||
'listen'
|
||||
]);
|
||||
mockDomainObject = jasmine.createSpyObj('domainObject', [
|
||||
'getCapability'
|
||||
'getCapability',
|
||||
'getModel'
|
||||
]);
|
||||
mockDomainObject.getCapability.andReturn(mockCapability);
|
||||
mockDomainObject.getModel.andReturn({});
|
||||
|
||||
mockScope = jasmine.createSpyObj('scope', [
|
||||
'$watchCollection'
|
||||
'$watchCollection',
|
||||
'$watch',
|
||||
'$on'
|
||||
]);
|
||||
mockScope.domainObject = mockDomainObject;
|
||||
|
||||
controller = new TableOptionsController(mockScope);
|
||||
});
|
||||
|
||||
it('Listens for changing domain object', function() {
|
||||
expect(mockScope.$watch).toHaveBeenCalledWith('domainObject', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('On destruction of controller, destroys listeners', function() {
|
||||
var unlistenFunc = jasmine.createSpy("unlisten");
|
||||
controller.listeners.push(unlistenFunc);
|
||||
expect(mockScope.$on).toHaveBeenCalledWith('$destroy', jasmine.any(Function));
|
||||
mockScope.$on.mostRecentCall.args[1]();
|
||||
expect(unlistenFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Registers a listener for mutation events on the object', function() {
|
||||
mockScope.$watch.mostRecentCall.args[1](mockDomainObject);
|
||||
expect(mockCapability.listen).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
2
platform/persistence/cache/README.md
vendored
2
platform/persistence/cache/README.md
vendored
@@ -1,2 +0,0 @@
|
||||
This bundle introduces a persistence cache to Open MCT Web to
|
||||
limit the number of persistence requests issued by the platform.
|
||||
@@ -1,161 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* This bundle decorates the persistence service to maintain a local cache
|
||||
* of persisted documents.
|
||||
* @namespace platform/persistence/cache
|
||||
*/
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* A caching persistence decorator maintains local copies of persistent objects
|
||||
* that have been loaded, and keeps them in sync after writes. This allows
|
||||
* retrievals to occur more quickly after the first load.
|
||||
*
|
||||
* @memberof platform/persistence/cache
|
||||
* @constructor
|
||||
* @param {string[]} cacheSpaces persistence space names which
|
||||
* should be cached
|
||||
* @param {PersistenceService} persistenceService the service which
|
||||
* implements object persistence, whose inputs/outputs
|
||||
* should be cached.
|
||||
* @implements {PersistenceService}
|
||||
*/
|
||||
function CachingPersistenceDecorator(cacheSpaces, persistenceService) {
|
||||
var spaces = cacheSpaces || [], // List of spaces to cache
|
||||
cache = {}; // Where objects will be stored
|
||||
|
||||
// Arrayify list of spaces to cache, if necessary.
|
||||
spaces = Array.isArray(spaces) ? spaces : [ spaces ];
|
||||
|
||||
// Initialize caches
|
||||
spaces.forEach(function (space) {
|
||||
cache[space] = {};
|
||||
});
|
||||
|
||||
this.spaces = spaces;
|
||||
this.cache = cache;
|
||||
this.persistenceService = persistenceService;
|
||||
}
|
||||
|
||||
// Wrap as a thenable; used instead of $q.when because that
|
||||
// will resolve on a future tick, which can cause latency
|
||||
// issues (which this decorator is intended to address.)
|
||||
function fastPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return fastPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Update the cached instance of an object to a new value
|
||||
function replaceValue(valueHolder, newValue) {
|
||||
var v = valueHolder.value;
|
||||
|
||||
// If it's a JS object, we want to replace contents, so that
|
||||
// everybody gets the same instance.
|
||||
if (typeof v === 'object' && v !== null) {
|
||||
// Only update contents if these are different instances
|
||||
if (v !== newValue) {
|
||||
// Clear prior contents
|
||||
Object.keys(v).forEach(function (k) {
|
||||
delete v[k];
|
||||
});
|
||||
// Shallow-copy contents
|
||||
Object.keys(newValue).forEach(function (k) {
|
||||
v[k] = newValue[k];
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Otherwise, just store the new value
|
||||
valueHolder.value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Place value in the cache for space, if there is one.
|
||||
CachingPersistenceDecorator.prototype.addToCache = function (space, key, value) {
|
||||
var cache = this.cache;
|
||||
if (cache[space]) {
|
||||
if (cache[space][key]) {
|
||||
replaceValue(cache[space][key], value);
|
||||
} else {
|
||||
cache[space][key] = { value: value };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Create a function for putting value into a cache;
|
||||
// useful for then-chaining.
|
||||
CachingPersistenceDecorator.prototype.putCache = function (space, key) {
|
||||
var self = this;
|
||||
return function (value) {
|
||||
self.addToCache(space, key, value);
|
||||
return value;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
CachingPersistenceDecorator.prototype.listSpaces = function () {
|
||||
return this.persistenceService.listSpaces();
|
||||
};
|
||||
|
||||
CachingPersistenceDecorator.prototype.listObjects = function (space) {
|
||||
return this.persistenceService.listObjects(space);
|
||||
};
|
||||
|
||||
CachingPersistenceDecorator.prototype.createObject = function (space, key, value) {
|
||||
this.addToCache(space, key, value);
|
||||
return this.persistenceService.createObject(space, key, value);
|
||||
};
|
||||
|
||||
CachingPersistenceDecorator.prototype.readObject = function (space, key) {
|
||||
var cache = this.cache;
|
||||
return (cache[space] && cache[space][key]) ?
|
||||
fastPromise(cache[space][key].value) :
|
||||
this.persistenceService.readObject(space, key)
|
||||
.then(this.putCache(space, key));
|
||||
};
|
||||
|
||||
CachingPersistenceDecorator.prototype.updateObject = function (space, key, value) {
|
||||
var self = this;
|
||||
return this.persistenceService.updateObject(space, key, value)
|
||||
.then(function (result) {
|
||||
self.addToCache(space, key, value);
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
CachingPersistenceDecorator.prototype.deleteObject = function (space, key, value) {
|
||||
if (this.cache[space]) {
|
||||
delete this.cache[space][key];
|
||||
}
|
||||
return this.persistenceService.deleteObject(space, key, value);
|
||||
};
|
||||
|
||||
return CachingPersistenceDecorator;
|
||||
}
|
||||
);
|
||||
@@ -1,142 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../src/CachingPersistenceDecorator"],
|
||||
function (CachingPersistenceDecorator) {
|
||||
|
||||
var PERSISTENCE_METHODS = [
|
||||
"listSpaces",
|
||||
"listObjects",
|
||||
"createObject",
|
||||
"readObject",
|
||||
"updateObject",
|
||||
"deleteObject"
|
||||
];
|
||||
|
||||
describe("The caching persistence decorator", function () {
|
||||
var testSpace,
|
||||
mockPersistence,
|
||||
mockCallback,
|
||||
decorator;
|
||||
|
||||
function mockPromise(value) {
|
||||
return {
|
||||
then: function (callback) {
|
||||
return mockPromise(callback(value));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
|
||||
testSpace = "TEST";
|
||||
mockPersistence = jasmine.createSpyObj(
|
||||
"persistenceService",
|
||||
PERSISTENCE_METHODS
|
||||
);
|
||||
mockCallback = jasmine.createSpy("callback");
|
||||
|
||||
PERSISTENCE_METHODS.forEach(function (m) {
|
||||
mockPersistence[m].andReturn(mockPromise({
|
||||
method: m
|
||||
}));
|
||||
});
|
||||
|
||||
decorator = new CachingPersistenceDecorator(
|
||||
testSpace,
|
||||
mockPersistence
|
||||
);
|
||||
});
|
||||
|
||||
it("delegates all methods", function () {
|
||||
PERSISTENCE_METHODS.forEach(function (m) {
|
||||
// Reset the callback
|
||||
mockCallback = jasmine.createSpy("callback");
|
||||
// Invoke the method; avoid using a key that will be cached
|
||||
decorator[m](testSpace, "testKey" + m, "testValue")
|
||||
.then(mockCallback);
|
||||
// Should have gotten that method's plain response
|
||||
expect(mockCallback).toHaveBeenCalledWith({ method: m });
|
||||
});
|
||||
});
|
||||
|
||||
it("does not repeat reads of cached objects", function () {
|
||||
// Perform two reads
|
||||
decorator.readObject(testSpace, "someKey", "someValue")
|
||||
.then(mockCallback);
|
||||
decorator.readObject(testSpace, "someKey", "someValue")
|
||||
.then(mockCallback);
|
||||
|
||||
// Should have only delegated once
|
||||
expect(mockPersistence.readObject.calls.length).toEqual(1);
|
||||
|
||||
// But both promises should have resolved
|
||||
expect(mockCallback.calls.length).toEqual(2);
|
||||
|
||||
});
|
||||
|
||||
it("gives a single instance of cached objects", function () {
|
||||
// Perform two reads
|
||||
decorator.readObject(testSpace, "someKey", "someValue")
|
||||
.then(mockCallback);
|
||||
decorator.readObject(testSpace, "someKey", "someValue")
|
||||
.then(mockCallback);
|
||||
|
||||
// Results should have been pointer-identical
|
||||
expect(mockCallback.calls[0].args[0])
|
||||
.toBe(mockCallback.calls[1].args[0]);
|
||||
});
|
||||
|
||||
it("maintains the same cached instance between reads/writes", function () {
|
||||
var testObject = { abc: "XYZ!" };
|
||||
|
||||
// Perform two reads with a write in between
|
||||
decorator.readObject(testSpace, "someKey", "someValue")
|
||||
.then(mockCallback);
|
||||
decorator.updateObject(testSpace, "someKey", testObject);
|
||||
decorator.readObject(testSpace, "someKey", "someValue")
|
||||
.then(mockCallback);
|
||||
|
||||
// Results should have been pointer-identical
|
||||
expect(mockCallback.calls[0].args[0])
|
||||
.toBe(mockCallback.calls[1].args[0]);
|
||||
|
||||
// But contents should have been equal to the written object
|
||||
expect(mockCallback).toHaveBeenCalledWith(testObject);
|
||||
});
|
||||
|
||||
it("is capable of reading/writing strings", function () {
|
||||
// Efforts made to keep cached objects pointer-identical
|
||||
// would break on strings - so make sure cache isn't
|
||||
// breaking when we read/write strings.
|
||||
decorator.createObject(testSpace, "someKey", "someValue");
|
||||
decorator.updateObject(testSpace, "someKey", "someOtherValue");
|
||||
decorator.readObject(testSpace, "someKey").then(mockCallback);
|
||||
expect(mockCallback).toHaveBeenCalledWith("someOtherValue");
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -97,9 +97,7 @@ define([
|
||||
"implementation": ContextMenuGesture,
|
||||
"depends": [
|
||||
"$timeout",
|
||||
"$parse",
|
||||
"agentService",
|
||||
"navigationService"
|
||||
"agentService"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -39,32 +39,16 @@ define(
|
||||
* in the context menu will be performed
|
||||
* @implements {Gesture}
|
||||
*/
|
||||
function ContextMenuGesture($timeout, $parse, agentService, navigationService, element, domainObject) {
|
||||
function ContextMenuGesture($timeout, agentService, element, domainObject) {
|
||||
var isPressing,
|
||||
longTouchTime = 500,
|
||||
parameters = element && element.attr('parameters') && $parse(element.attr('parameters'))();
|
||||
|
||||
function suppressMenu() {
|
||||
return parameters &&
|
||||
parameters.suppressMenuOnEdit &&
|
||||
navigationService.getNavigation() &&
|
||||
navigationService.getNavigation().hasCapability('editor');
|
||||
}
|
||||
longTouchTime = 500;
|
||||
|
||||
function showMenu(event) {
|
||||
/**
|
||||
* Some menu items should have the context menu action
|
||||
* suppressed (eg. the navigation menu on the left)
|
||||
*/
|
||||
if (suppressMenu()){
|
||||
return;
|
||||
} else {
|
||||
domainObject.getCapability('action').perform({
|
||||
key: 'menu',
|
||||
domainObject: domainObject,
|
||||
event: event
|
||||
});
|
||||
}
|
||||
domainObject.getCapability('action').perform({
|
||||
key: 'menu',
|
||||
domainObject: domainObject,
|
||||
event: event
|
||||
});
|
||||
}
|
||||
|
||||
// When context menu event occurs, show object actions instead
|
||||
|
||||
@@ -28,16 +28,14 @@ define(
|
||||
["../../src/gestures/ContextMenuGesture"],
|
||||
function (ContextMenuGesture) {
|
||||
|
||||
var JQLITE_FUNCTIONS = [ "on", "off", "find", "append", "remove", "attr" ],
|
||||
var JQLITE_FUNCTIONS = [ "on", "off", "find", "append", "remove" ],
|
||||
DOMAIN_OBJECT_METHODS = [ "getId", "getModel", "getCapability", "hasCapability", "useCapability"];
|
||||
|
||||
|
||||
describe("The 'context menu' gesture", function () {
|
||||
var mockTimeout,
|
||||
mockParse,
|
||||
mockElement,
|
||||
mockAgentService,
|
||||
mockNavigationService,
|
||||
mockDomainObject,
|
||||
mockEvent,
|
||||
mockTouchEvent,
|
||||
@@ -51,7 +49,6 @@ define(
|
||||
|
||||
beforeEach(function () {
|
||||
mockTimeout = jasmine.createSpy("$timeout");
|
||||
mockParse = jasmine.createSpy("$parse");
|
||||
mockElement = jasmine.createSpyObj("element", JQLITE_FUNCTIONS);
|
||||
mockAgentService = jasmine.createSpyObj("agentService", ["isMobile"]);
|
||||
mockDomainObject = jasmine.createSpyObj("domainObject", DOMAIN_OBJECT_METHODS);
|
||||
@@ -60,17 +57,14 @@ define(
|
||||
"action",
|
||||
[ "perform", "getActions" ]
|
||||
);
|
||||
mockActionContext = jasmine.createSpyObj(
|
||||
"actionContext",
|
||||
[ "" ]
|
||||
);
|
||||
|
||||
mockActionContext = {domainObject: mockDomainObject, event: mockEvent};
|
||||
mockDomainObject.getCapability.andReturn(mockContextMenuAction);
|
||||
mockContextMenuAction.perform.andReturn(jasmine.any(Function));
|
||||
mockAgentService.isMobile.andReturn(false);
|
||||
|
||||
gesture = new ContextMenuGesture(mockTimeout, mockParse, mockAgentService, mockNavigationService, mockElement, mockDomainObject);
|
||||
|
||||
gesture = new ContextMenuGesture(mockTimeout, mockAgentService, mockElement, mockDomainObject);
|
||||
|
||||
// Capture the contextmenu callback
|
||||
fireGesture = mockElement.on.mostRecentCall.args[1];
|
||||
@@ -106,7 +100,7 @@ define(
|
||||
mockAgentService.isMobile.andReturn(true);
|
||||
|
||||
// Then create new (mobile) gesture
|
||||
gesture = new ContextMenuGesture(mockTimeout, mockParse, mockAgentService, mockNavigationService, mockElement, mockDomainObject);
|
||||
gesture = new ContextMenuGesture(mockTimeout, mockAgentService, mockElement, mockDomainObject);
|
||||
|
||||
// Set calls for the touchstart and touchend gestures
|
||||
fireTouchStartGesture = mockElement.on.calls[1].args[1];
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
define([
|
||||
"./src/controllers/SearchController",
|
||||
"./src/controllers/SearchMenuController",
|
||||
"./src/controllers/ClickAwayController",
|
||||
"./src/services/GenericSearchProvider",
|
||||
"./src/services/SearchAggregator",
|
||||
"text!./res/templates/search-item.html",
|
||||
@@ -33,7 +32,6 @@ define([
|
||||
], function (
|
||||
SearchController,
|
||||
SearchMenuController,
|
||||
ClickAwayController,
|
||||
GenericSearchProvider,
|
||||
SearchAggregator,
|
||||
searchItemTemplate,
|
||||
@@ -71,14 +69,6 @@ define([
|
||||
"$scope",
|
||||
"types[]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "ClickAwayController",
|
||||
"implementation": ClickAwayController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"$document"
|
||||
]
|
||||
}
|
||||
],
|
||||
"representations": [
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
* Copied from the ClickAwayController in platform/commonUI/general
|
||||
*/
|
||||
|
||||
define(
|
||||
[],
|
||||
function () {
|
||||
|
||||
/**
|
||||
* A ClickAwayController is used to toggle things (such as context
|
||||
* menus) where clicking elsewhere in the document while the toggle
|
||||
* is in an active state is intended to dismiss the toggle.
|
||||
*
|
||||
* @constructor
|
||||
* @param $scope the scope in which this controller is active
|
||||
* @param $document the document element, injected by Angular
|
||||
*/
|
||||
function ClickAwayController($scope, $document) {
|
||||
var state = false,
|
||||
clickaway;
|
||||
|
||||
// Track state, but also attach and detach a listener for
|
||||
// mouseup events on the document.
|
||||
function deactivate() {
|
||||
state = false;
|
||||
$document.off("mouseup", clickaway);
|
||||
}
|
||||
|
||||
function activate() {
|
||||
state = true;
|
||||
$document.on("mouseup", clickaway);
|
||||
}
|
||||
|
||||
function changeState() {
|
||||
if (state) {
|
||||
deactivate();
|
||||
} else {
|
||||
activate();
|
||||
}
|
||||
}
|
||||
|
||||
// Callback used by the document listener. Deactivates;
|
||||
// note also $scope.$apply is invoked to indicate that
|
||||
// the state of this controller has changed.
|
||||
clickaway = function () {
|
||||
deactivate();
|
||||
$scope.$apply();
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Get the current state of the toggle.
|
||||
* @return {boolean} true if active
|
||||
*/
|
||||
isActive: function () {
|
||||
return state;
|
||||
},
|
||||
/**
|
||||
* Set a new state for the toggle.
|
||||
* @return {boolean} true to activate
|
||||
*/
|
||||
setState: function (newState) {
|
||||
if (state !== newState) {
|
||||
changeState();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Toggle the current state; activate if it is inactive,
|
||||
* deactivate if it is active.
|
||||
*/
|
||||
toggle: function () {
|
||||
changeState();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return ClickAwayController;
|
||||
}
|
||||
);
|
||||
@@ -1,92 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2015, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT Web includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
["../../src/controllers/ClickAwayController"],
|
||||
function (ClickAwayController) {
|
||||
|
||||
describe("The click-away controller", function () {
|
||||
var mockScope,
|
||||
mockDocument,
|
||||
controller;
|
||||
|
||||
beforeEach(function () {
|
||||
mockScope = jasmine.createSpyObj(
|
||||
"$scope",
|
||||
[ "$apply" ]
|
||||
);
|
||||
mockDocument = jasmine.createSpyObj(
|
||||
"$document",
|
||||
[ "on", "off" ]
|
||||
);
|
||||
controller = new ClickAwayController(mockScope, mockDocument);
|
||||
});
|
||||
|
||||
it("is initially inactive", function () {
|
||||
expect(controller.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it("does not listen to the document before being toggled", function () {
|
||||
expect(mockDocument.on).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("tracks enabled/disabled state when toggled", function () {
|
||||
controller.toggle();
|
||||
expect(controller.isActive()).toBe(true);
|
||||
controller.toggle();
|
||||
expect(controller.isActive()).toBe(false);
|
||||
controller.toggle();
|
||||
expect(controller.isActive()).toBe(true);
|
||||
controller.toggle();
|
||||
expect(controller.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it("allows active state to be explictly specified", function () {
|
||||
controller.setState(true);
|
||||
expect(controller.isActive()).toBe(true);
|
||||
controller.setState(true);
|
||||
expect(controller.isActive()).toBe(true);
|
||||
controller.setState(false);
|
||||
expect(controller.isActive()).toBe(false);
|
||||
controller.setState(false);
|
||||
expect(controller.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it("registers a mouse listener when activated", function () {
|
||||
controller.setState(true);
|
||||
expect(mockDocument.on).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deactivates and detaches listener on document click", function () {
|
||||
var callback;
|
||||
controller.setState(true);
|
||||
callback = mockDocument.on.mostRecentCall.args[1];
|
||||
callback();
|
||||
expect(controller.isActive()).toEqual(false);
|
||||
expect(mockDocument.off).toHaveBeenCalledWith("mouseup", callback);
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user