I'm creating a map editing webapp where we can create and edit polylines, polygons etc. I've some trouble finding informations on undo implementation on the web, I find whining about "we need undo" and "here is my Command pattern using closures" but I think between that and a full undo/redo interface there is quite some road.
So, here are my questions (good candidate for wiki I think):
Should I manage the stack, or is there a way to send my commands to the browser's stack ? (and how do I handle native commands, like text edits in textifields in this case)
how do I handle "command compression" (command grouping) when some commands are browser native
How do I detect the undo (ctrl+z) keystroke?
If I register a keyup event, how do I decide if I prevent default or not?
If not, can I register some undoevent handler somewhere ?
Users are not used to undo on the web, how can I "train" them to explore/undo on my application ?
You need to have functions for object creation and deletion. Then pass those functions to the undo manager. See the demo file of my javascript undo manager: https://github.com/ArthurClemens/Javascript-Undo-Manager
The demo code shows canvas, but the code is agnostic.
It doesn't contain key bindings, but may help you with the first steps.
Myself I have used this in a web application with buttons for undo and redo, next to save.
Here is a sample of N-Level undo using Knockout JS:
(function() {
//current state would probably come from the server, hard coded here for example
var currentState = JSON.stringify({
firstName: 'Paul',
lastName: 'Tyng',
text: 'Text'
})
, undoStack = [] //this represents all the previous states of the data in JSON format
, performingUndo = false //flag indicating in the middle of an undo, to skip pushing to undoStack when resetting properties
, viewModel = ko.mapping.fromJSON(currentState); //enriching of state with observables
//this creates a dependent observable subscribed to all observables
//in the view (toJS is just a shorthand to traverse all the properties)
//the dependent observable is then subscribed to for pushing state history
ko.dependentObservable(function() {
ko.toJS(viewModel); //subscribe to all properties
}, viewModel).subscribe(function() {
if(!performingUndo) {
undoStack.push(currentState);
currentState = ko.mapping.toJSON(viewModel);
}
});
//pops state history from undoStack, if its the first entry, just retrieve it
window.undo = function() {
performingUndo = true;
if(undoStack.length > 1)
{
currentState = undoStack.pop();
ko.mapping.fromJSON(currentState, {}, viewModel);
}
else {
currentState = undoStack[0];
ko.mapping.fromJSON(undoStack[0], {}, viewModel);
}
performingUndo = false;
};
ko.applyBindings(viewModel);
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/1.2.1/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.6.3/jquery.min.js"></script>
<div>
<button data-bind="click: function() { undo(); }">Undo</button>
<input data-bind="value: firstName" />
<input data-bind="value: lastName" />
<textarea data-bind="value: text"></textarea>
</div>
It uses an MVVM model so your page state is represented in a javascript object that it maintains a history for.
The way Cappuccino's automatic undo support works is by telling the undo manager what properties should be undoable. For example, pretend you are managing records of students, you might do something like:
[theUndoManager observeChangesForKeyPath:#"firstName" ofObject:theStudent];
[theUndoManager observeChangesForKeyPath:#"lastName" ofObject:theStudent];
Now regardless of how the students name is changed in the UI, hitting undo will automatically revert it back. Cappuccino also automatically handles coalescing changes in the same run loop, marking the document as "dirty" (needing save) when there are items on the undo stack, etc etc (in other words, the above should be ALL you need to do to support undo).
As another example, if you wanted to make additions and deletions of students undoable, you'd do the following:
[theUndoManager observeChangesForKeyPath:#"students" ofObject:theClass];
Since "students" is an array of students in theClass, then additions and deletions from this array will be tracked.
Related
I have an existing Quick Action button on an object, which I would like to display as a button for Community Users
I have tried implementing lightning:quickActionAPI in a Lightning Component that I created, then added the component to the record detail page in Community Builder. I have changed the actual names of objects and fields with general names
<lightning:quickActionAPI aura:id="quickActionAPI" />
<lightning:button label="Update" onclick="{!c.updateRequestStatus }" />
updateRequestStatus : function(component, event, helper) {
//debugger;
var actionAPI = component.find("quickActionAPI");
var fields = {fieldApiName: {value:"Closed"}};
var args = {actionName: "objectApiName.quickActionName", entityName: "objectApiName", targetFields: fields};
actionAPI.setActionFieldValues(args).then(function(){
actionAPI.invokeAction(args);
}).catch(function(e){
console.error(e.errors);
});
}
Expected result: when clicking on the button in the community, the quick action will be called and a window will open
Actual result: clicking on the button executes the JS method but nothing happens
I'm currently seeing the same thing in my Community. The documentation says lightning:quickActionAPI is only available in Lightning Experience, and makes no references to Communities. I don't think it's supported, yet. Though it is confusing that the actionAPI object will instantiate just fine in a Community context but its promises never complete.
On a rather big page a user can edit data in various ways:
- Change some "classical" input fields
- Add files via clicking on a button
- Change the order of items via drag and drop
In the HTML, a simplified example might look like such:
<form>
Name: <input ng-model="person.name">
Title: <input ng-model="person.title">
Image: <our-custom-image-uploade-directive ng-model="person.image"/>
Skills: <our-custom-skill-drag-and-drop-directive ng-model="person.skills"/>
<button ng-click="save()">Save</button>
</form>
You see, some edit "facilities" are form-based, some are not.
Now, there is a rather simple task to do: Disable the "save" button if the user didn't change anything or happened to end up with the very same state of data that it was before the user interacted with the data.
Now I'm wondering which is the best way to achieve that.
One way might be to deep watch the whole person object. Like that:
$scope.backupCopyOfPerson = angular.copy($scope.person); // Creae a backup copy of the state before the user changed something
$scope.$watch('person', function (newValue) {
if(newValue && $scope.backupCopyOfPerson) {
if(angular.equals(newValue, $scope.backupCopyOfPerson)) {
$scope.unsavedChanges = false;
}
else {
$scope.unsavedChanges = true;
}
}
}, true);
However, deep watching a big object with a lot of sub-objects etc. might cause some serious performance issues.
Another idea is, using ng-pristine for the vanilla form fields and do in all other directives etc. $setDirty()/$setPristine(). That might be faster, but it's definitely not an elegant solution.
What do you think?
Angular version is 1.58
My take on this:
If you have issue with the watching performance:
You should combine angular form checking for regular input, and add a watcher on the other attributes.
This will save you time & performances as you will watch the variable that are not already
$scope.$watch('person.image', function (newValue) {
$scope.unsavedChanges.image = ($scope.person.image == $scope.backupCopyOfPerson.image)
}}, true);
and then upon save check if your unsavedChanges as at least 1 true
I'm curious about the possibility of damaging localStorage entry by overwriting it in two browser tabs simultaneously. Should I create a mutex for local storage?
I was already thinking of such pseudo-class:
LocalStorageMan.prototype.v = LocalStorageMan.prototype.value = function(name, val) {
//Set inner value
this.data[name] = val;
//Delay any changes if the local storage is being changed
if(localStorage[this.name+"__mutex"]==1) {
setTimeout(function() {this.v(name, val);}, 1);
return null; //Very good point #Lightness Races in Orbit
}
//Lock the mutext to prevent overwriting
localStorage[this.name+"__mutex"] = 1;
//Save serialized data
localStorage[this.name] = this.serializeData;
//Allow usage from another tabs
localStorage[this.name+"__mutex"] = 0;
}
The function above implies local storage manager that is managing one specific key of the local storage - localStorage["test"] for example. I want to use this for greasomonkey userscripts where avoiding conlicts is a priority.
Yes, it is thread safe. However, your code isn't atomic and that's your problem there. I'll get to thread safety of localStorage but first, how to fix your problem.
Both tabs can pass the if check together and write to the item overwriting each other. The correct way to handle this problem is using StorageEvents.
These let you notify other windows when a key has changed in localStorage, effectively solving the problem for you in a built in message passing safe way. Here is a nice read about them. Let's give an example:
// tab 1
localStorage.setItem("Foo","Bar");
// tab 2
window.addEventListener("storage",function(e){
alert("StorageChanged!"); // this will run when the localStorage is changed
});
Now, what I promised about thread safety :)
As I like - let's observe this from two angles - from the specification and using the implementation.
The specification
Let's show it's thread safe by specification.
If we check the specification of Web Storage we can see that it specifically notes:
Because of the use of the storage mutex, multiple browsing contexts will be able to access the local storage areas simultaneously in such a manner that scripts cannot detect any concurrent script execution.
Thus, the length attribute of a Storage object, and the value of the various properties of that object, cannot change while a script is executing, other than in a way that is predictable by the script itself.
It even elaborates further:
Whenever the properties of a localStorage attribute's Storage object are to be examined, returned, set, or deleted, whether as part of a direct property access, when checking for the presence of a property, during property enumeration, when determining the number of properties present, or as part of the execution of any of the methods or attributes defined on the Storage interface, the user agent must first obtain the storage mutex.
Emphasis mine. It also notes that some implementors don't like this as a note.
In practice
Let's show it's thread safe in implementation.
Choosing a random browser, I chose WebKit (because I didn't know where that code is located there before). If we check at WebKit's implementation of Storage we can see that it has its fare share of mutexes.
Let's take it from the start. When you call setItem or assign, this happens:
void Storage::setItem(const String& key, const String& value, ExceptionCode& ec)
{
if (!m_storageArea->canAccessStorage(m_frame)) {
ec = SECURITY_ERR;
return;
}
if (isDisabledByPrivateBrowsing()) {
ec = QUOTA_EXCEEDED_ERR;
return;
}
bool quotaException = false;
m_storageArea->setItem(m_frame, key, value, quotaException);
if (quotaException)
ec = QUOTA_EXCEEDED_ERR;
}
Next, this happens in StorageArea:
void StorageAreaImpl::setItem(Frame* sourceFrame, const String& key, const String& value, bool& quotaException)
{
ASSERT(!m_isShutdown);
ASSERT(!value.isNull());
blockUntilImportComplete();
String oldValue;
RefPtr<StorageMap> newMap = m_storageMap->setItem(key, value, oldValue, quotaException);
if (newMap)
m_storageMap = newMap.release();
if (quotaException)
return;
if (oldValue == value)
return;
if (m_storageAreaSync)
m_storageAreaSync->scheduleItemForSync(key, value);
dispatchStorageEvent(key, oldValue, value, sourceFrame);
}
Note that blockUntilImportComplete here. Let's look at that:
void StorageAreaSync::blockUntilImportComplete()
{
ASSERT(isMainThread());
// Fast path. We set m_storageArea to 0 only after m_importComplete being true.
if (!m_storageArea)
return;
MutexLocker locker(m_importLock);
while (!m_importComplete)
m_importCondition.wait(m_importLock);
m_storageArea = 0;
}
They also went as far as add a nice note:
// FIXME: In the future, we should allow use of StorageAreas while it's importing (when safe to do so).
// Blocking everything until the import is complete is by far the simplest and safest thing to do, but
// there is certainly room for safe optimization: Key/length will never be able to make use of such an
// optimization (since the order of iteration can change as items are being added). Get can return any
// item currently in the map. Get/remove can work whether or not it's in the map, but we'll need a list
// of items the import should not overwrite. Clear can also work, but it'll need to kill the import
// job first.
Explaining this works, but it can be more efficient.
No, it's not. Mutex was removed from the spec, and this warning was added instead:
The localStorage getter provides access to shared state. This
specification does not define the interaction with other browsing
contexts in a multiprocess user agent, and authors are encouraged to
assume that there is no locking mechanism. A site could, for instance,
try to read the value of a key, increment its value, then write it
back out, using the new value as a unique identifier for the session;
if the site does this twice in two different browser windows at the
same time, it might end up using the same "unique" identifier for both
sessions, with potentially disastrous effects.
See HTML Spec: 12 Web storage
I am using CRM 2013 on-premise with UR1 installed
I have a custom entity with a subgrid on it looking at related "tasks" which looks like this:
Whenever I create a task from the subgrid using the "+" button in the top right hand corner of the subgrid; the "Regarding" field of the newly created task remains blank. When it should be populated by a lookup to the record it was created from.
I have javascript on the task entity which checks the "Regarding" field to check what kind of entity it was created from (if it was created from one) and gets certain field values from the calling entity to populate fields on the task.
Since the "Regarding" field is never filled the Javascript never fires - and the fields do not populate.
When the record is saved, if the regarding field is blank (I have not manually filled it in) - it will eventually be populated by the correct record about 10 - 15 seconds later if you refresh the page. Then the correct fields will be populated and the user is able to edit the option set values and save again. This is not ideal for the user as they would like it to be one fluid action.
Is there any way around this problem?
EDIT for future browsers of this question:
Found a partial work around. If you use an "Activity" subgrid rather than a "Task" subgrid the field will populate. This has a drawback though as you cannot edit the "Activity" subgrid's view to show "Task" specific fields.
Ran into this same issue. The way I got around it was to add a look-up to the custom entity on the form (we put this on a hidden tab). When the Task gets created from the custom entity the look-up will be populated. You can then use that look-up to grab the values that you need to populate, including the regarding field. Not the most elegant, but it works.
I also ran into this problem and went with a pure JS approach to resolving. On load of the task form, call populateRegarding().
This works because even though the regarding lookup doesn't populate by default, the query string parameters include _CreateFromType and _CreateFromId values.
This works in 2015, didn't test on earlier versions. Note that it is unsupported.
function populateRegarding() {
var regarding = Xrm.Page.getAttribute("regardingobjectid"),
createFromType = Xrm.Page.context.getQueryStringParameters()._CreateFromType,
createFromId = Xrm.Page.context.getQueryStringParameters()._CreateFromId;
if (!createFromId || !createFromType ||
!regarding || regarding.getValue() !== null) {
return;
}
var entityLogicalName = getEntityLogicalNameFromObjectTypeCode(createFromType);
regarding.setValue([{
id: createFromId,
entityType: entityLogicalName,
name: "Hardcoded Name" // TODO: retrieve name dynamically
}]);
}
// This method uses an undocumented object and is therefore unsupported.
// You could implement a supported version of this function by querying for
// metadata, but that would be very expensive.
function getEntityLogicalNameFromObjectTypeCode(otc) {
var map = Mscrm.EntityPropUtil.EntityTypeName2CodeMap,
logicalName;
otc = Number(otc); // convert string to number
for (logicalName in map) {
if (!map.hasOwnProperty(logicalName)) { continue; }
if (map[logicalName] === otc) {
return logicalName;
}
}
}
just a very short question on using Backbone.js with LocalStorage:
I'm storing a list of things (Backbone collection) in LocalStorage. When my website is open in multiple browser windows / tabs and the user in both windows adds something to the list, one window's changes will overwrite the changes made in the other window.
If you want to try for yourself, just use the example Backbone.js Todo app:
Open http://backbonejs.org/examples/todos/index.html in two browser tabs
Add an item 'item1' in the first tab and 'item2' in the second tab
Refresh both tabs: 'item1' will disappear and you'll be left with 'item2' only
Any suggestions how to prevent this from happening, any standard way to deal with this?
Thxx
The issue is well-known concurrency lost updates problem, see Lost update in Concurrency control?.
Just for your understanding I might propose the following quick and dirty fix, file backbone-localstorage.js, Store.prototype.save:
save: function() {
// reread data right before writing
var store = localStorage.getItem(this.name);
var data = (store && JSON.parse(store)) || {};
// we may choose what is overwritten with what here
_.extend(this.data, data);
localStorage.setItem(this.name, JSON.stringify(this.data));
}
For the latest Github version of Backbone localStorage, I think this should look like this:
save: function() {
var store = this.localStorage().getItem(this.name);
var records = (store && store.split(",")) || [];
var all = _.union(records, this.records);
this.localStorage().setItem(this.name, all.join(","));
}
You may want to use sessionStorage instead.
See http://en.wikipedia.org/wiki/Web_storage#Local_and_session_storage.
Yaroslav's comment about checking for changes before persisting new ones is one solution but my suggestion would be different. Remember that localStorage is capable of firing events when it performs actions that change the data it holds. Bind to those events and have each tab listen for those changes and view re-render after it happens.
Then, when I make deletions or additions in one tab and move over to the next, it will get an event and change to reflect what happened in the other tab. There won't be weird discrepancies in what I'm seeing tab to tab.
You will want to give some thought to making sure that I don't lose something I was in the middle of adding (say I start typing a new entry for my to-do list), switch to another tab and delete something, and then come back I want to see the entry disappear but my partially typed new item should still be available for me.