Skip to content
This repository was archived by the owner on Jul 25, 2019. It is now read-only.

Commit b5b46ae

Browse files
committedMay 28, 2013
feature: initial release of observeChanges option
1 parent f1bc96e commit b5b46ae

File tree

5 files changed

+360
-0
lines changed

5 files changed

+360
-0
lines changed
 

Diff for: ‎CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
Features:
22
- initial release of Native Scrollbars feature (experimental, don't use yet)
3+
- initial release of `observeChanges` option (experimental). Available for all supported browsers except IE 7-8 (uses Object.observe when possible or a custom shim based on [Starcounter-Jack/JSON-Patch](https://github.com/Starcounter-Jack/JSON-Patch))
34

45
Bugfixes:
56
- mouse wheel didn't scroll the window when cursor was over Handsontable ([#383](https://github.com/warpech/jquery-handsontable/issues/383), [#627](https://github.com/warpech/jquery-handsontable/issues/627))

Diff for: ‎Gruntfile.js

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ module.exports = function (grunt) {
5252
'src/plugins/legacy.js',
5353
'src/plugins/manualColumnMove.js',
5454
'src/plugins/manualColumnResize.js',
55+
'src/plugins/observeChanges.js',
5556

5657
'src/3rdparty/jquery.autoresize.js',
5758
'src/3rdparty/sheetclip.js',

Diff for: ‎src/plugins/observeChanges.js

+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
function HandsontableObserveChanges() {
2+
// begin shim code
3+
// fragments from https://github.com/Starcounter-Jack/JSON-Patch/blob/master/src/json-patch-duplex.js
4+
//
5+
// json-patch.js 0.3
6+
// (c) 2013 Joachim Wester
7+
// MIT license
8+
var observeOps = {
9+
'new': function (patches, path) {
10+
var patch = {
11+
op: "add",
12+
path: path + "/" + this.name,
13+
value: this.object[this.name]
14+
};
15+
patches.push(patch);
16+
},
17+
deleted: function (patches, path) {
18+
var patch = {
19+
op: "remove",
20+
path: path + "/" + this.name
21+
};
22+
patches.push(patch);
23+
},
24+
updated: function (patches, path) {
25+
var patch = {
26+
op: "replace",
27+
path: path + "/" + this.name,
28+
value: this.object[this.name]
29+
};
30+
patches.push(patch);
31+
}
32+
};
33+
// ES6 symbols are not here yet. Used to calculate the json pointer to each object
34+
function markPaths(observer, node) {
35+
for (var key in node) {
36+
var kid = node[key];
37+
if (kid instanceof Object) {
38+
Object.unobserve(kid, observer);
39+
kid.____Path = node.____Path + "/" + key;
40+
markPaths(observer, kid);
41+
}
42+
}
43+
}
44+
45+
// Detach poor mans ES6 symbols
46+
function clearPaths(observer, node) {
47+
delete node.____Path;
48+
Object.observe(node, observer);
49+
for (var key in node) {
50+
var kid = node[key];
51+
if (kid instanceof Object) {
52+
clearPaths(observer, kid);
53+
}
54+
}
55+
}
56+
57+
var beforeDict = [];
58+
var callbacks = [];
59+
60+
function observe(obj, callback) {
61+
var patches = [];
62+
var root = obj;
63+
if (Object.observe) {
64+
var observer = function (arr) {
65+
if (!root.___Path) {
66+
Object.unobserve(root, observer);
67+
root.____Path = "";
68+
markPaths(observer, root);
69+
arr.forEach(function (elem) {
70+
if (elem.name != "____Path") {
71+
observeOps[elem.type].call(elem, patches, elem.object.____Path);
72+
}
73+
});
74+
clearPaths(observer, root);
75+
}
76+
if (callback) {
77+
callback.call(patches);
78+
}
79+
};
80+
} else {
81+
observer = {
82+
};
83+
84+
var found;
85+
for (var i = 0, ilen = beforeDict.length; i < ilen; i++) {
86+
if (beforeDict[i].obj === obj) {
87+
found = beforeDict[i];
88+
break;
89+
}
90+
}
91+
92+
if (!found) {
93+
found = {obj: obj};
94+
beforeDict.push(found);
95+
}
96+
97+
found.value = JSON.parse(JSON.stringify(obj))// Faster than ES5 clone
98+
99+
if (callback) {
100+
callbacks.push(callback);
101+
var next;
102+
var intervals = [
103+
100
104+
];
105+
var currentInterval = 0;
106+
var dirtyCheck = function () {
107+
var temp = generate(observer);
108+
if (temp.length > 0) {
109+
observer.patches = [];
110+
callback.call(null, temp);
111+
}
112+
};
113+
var fastCheck = function (e) {
114+
clearTimeout(next);
115+
next = setTimeout(function () {
116+
dirtyCheck();
117+
currentInterval = 0;
118+
next = setTimeout(slowCheck, intervals[currentInterval++]);
119+
}, 0);
120+
};
121+
var slowCheck = function () {
122+
dirtyCheck();
123+
if (currentInterval == intervals.length) {
124+
currentInterval = intervals.length - 1;
125+
}
126+
next = setTimeout(slowCheck, intervals[currentInterval++]);
127+
};
128+
[
129+
"mousedown",
130+
"mouseup",
131+
"keydown"
132+
].forEach(function (str) {
133+
window.addEventListener(str, fastCheck);
134+
});
135+
next = setTimeout(slowCheck, intervals[currentInterval++]);
136+
}
137+
}
138+
observer.patches = patches;
139+
observer.object = obj;
140+
return _observe(observer, obj, patches);
141+
}
142+
143+
/// Listen to changes on an object tree, accumulate patches
144+
function _observe(observer, obj, patches) {
145+
if (Object.observe) {
146+
Object.observe(obj, observer);
147+
}
148+
for (var key in obj) {
149+
if (obj.hasOwnProperty(key)) {
150+
var v = obj[key];
151+
if (v && typeof (v) === "object") {
152+
_observe(observer, v, patches)//path+key);
153+
;
154+
}
155+
}
156+
}
157+
return observer;
158+
}
159+
160+
function generate(observer) {
161+
if (Object.observe) {
162+
Object.deliverChangeRecords(observer);
163+
} else {
164+
var mirror;
165+
for (var i = 0, ilen = beforeDict.length; i < ilen; i++) {
166+
if (beforeDict[i].obj === observer.object) {
167+
mirror = beforeDict[i];
168+
break;
169+
}
170+
}
171+
_generate(mirror, observer.object, observer.patches, "");
172+
}
173+
return observer.patches;
174+
}
175+
176+
function _generate(mirror, obj, patches, path) {
177+
var newKeys = Object.keys(obj);
178+
var oldKeys = Object.keys(mirror);
179+
var changed = false;
180+
var deleted = false;
181+
var added = false;
182+
for (var t = 0; t < oldKeys.length; t++) {
183+
var key = oldKeys[t];
184+
var oldVal = mirror[key];
185+
if (obj.hasOwnProperty(key)) {
186+
var newVal = obj[key];
187+
if (oldVal instanceof Object) {
188+
_generate(oldVal, newVal, patches, path + "/" + key);
189+
} else {
190+
if (oldVal != newVal) {
191+
changed = true;
192+
patches.push({
193+
op: "replace",
194+
path: path + "/" + key,
195+
value: newVal
196+
});
197+
mirror[key] = newVal;
198+
}
199+
}
200+
} else {
201+
patches.push({
202+
op: "remove",
203+
path: path + "/" + key
204+
});
205+
deleted = true// property has been deleted
206+
;
207+
}
208+
}
209+
if (!deleted && newKeys.length == oldKeys.length) {
210+
return;
211+
}
212+
for (var t = 0; t < newKeys.length; t++) {
213+
var key = newKeys[t];
214+
if (!mirror.hasOwnProperty(key)) {
215+
patches.push({
216+
op: "add",
217+
path: path + "/" + key,
218+
value: obj[key]
219+
});
220+
}
221+
}
222+
}
223+
//end shim code
224+
225+
226+
this.afterLoadData = function () {
227+
if (!this.observer && this.getSettings().observeChanges) {
228+
var that = this;
229+
this.observer = observe(this.getData(), function () {
230+
that.render();
231+
});
232+
}
233+
};
234+
}
235+
var htObserveChanges = new HandsontableObserveChanges();
236+
237+
Handsontable.PluginHooks.add('afterLoadData', htObserveChanges.afterLoadData);

Diff for: ‎test/jasmine/SpecRunner.html

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
<script type="text/javascript" src="spec/plugins/autoColumnSizeSpec.js"></script>
6868
<script type="text/javascript" src="spec/plugins/contextMenuSpec.js"></script>
6969
<script type="text/javascript" src="spec/plugins/columnSortingSpec.js"></script>
70+
<script type="text/javascript" src="spec/plugins/observeChangesSpec.js"></script>
7071
<script type="text/javascript" src="spec/plugins/PluginHooksSpec.js"></script>
7172

7273
<script type="text/javascript" src="spec/settings/editorSpec.js"></script>

Diff for: ‎test/jasmine/spec/plugins/observeChangesSpec.js

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
describe('HandsontableObserveChanges', function () {
2+
var id = 'testContainer';
3+
4+
beforeEach(function () {
5+
this.$container = $('<div id="' + id + '"></div>').appendTo('body');
6+
});
7+
8+
afterEach(function () {
9+
if (this.$container) {
10+
destroy();
11+
this.$container.remove();
12+
}
13+
});
14+
15+
function createHOT(data, observeChanges) {
16+
handsontable({
17+
data: data,
18+
width: 200,
19+
height: 200,
20+
observeChanges: observeChanges
21+
});
22+
}
23+
24+
it('should render newly added row', function () {
25+
var data = createSpreadsheetData(2, 2);
26+
createHOT(data, true);
27+
data.push(["A3", "B3"]);
28+
29+
waits(100); //Object.observe is async
30+
31+
runs(function () {
32+
expect(this.$container.find('tr').length).toEqual(3);
33+
expect(this.$container.find('col').length).toEqual(2);
34+
});
35+
});
36+
37+
it('should render newly added column', function () {
38+
var data = createSpreadsheetData(2, 2);
39+
createHOT(data, true);
40+
data[0].push("C1");
41+
data[1].push("C2");
42+
43+
waits(100); //Object.observe is async
44+
45+
runs(function () {
46+
expect(this.$container.find('tr').length).toEqual(2);
47+
expect(this.$container.find('col').length).toEqual(3);
48+
});
49+
});
50+
51+
it('should render removed row', function () {
52+
var data = createSpreadsheetData(2, 2);
53+
createHOT(data, true);
54+
data.splice(0, 1); //removes one row at index 0
55+
56+
waits(100); //Object.observe is async
57+
58+
runs(function () {
59+
expect(this.$container.find('tr').length).toEqual(1);
60+
expect(this.$container.find('col').length).toEqual(2);
61+
});
62+
});
63+
64+
it('should render removed column', function () {
65+
var data = createSpreadsheetData(2, 2);
66+
createHOT(data, true);
67+
data[0].splice(0, 1); //removes one column at index 0 in first row
68+
data[1].splice(0, 1); //removes one column at index 0 in second row
69+
70+
waits(100); //Object.observe is async
71+
72+
runs(function () {
73+
expect(this.$container.find('tr').length).toEqual(2);
74+
expect(this.$container.find('col').length).toEqual(1);
75+
});
76+
});
77+
78+
it('should render cell change from string to string', function () {
79+
var data = createSpreadsheetData(2, 2);
80+
createHOT(data, true);
81+
data[0][0] = 'new string';
82+
83+
waits(100); //Object.observe is async
84+
85+
runs(function () {
86+
expect(this.$container.find('td:eq(0)').html()).toEqual('new string');
87+
});
88+
});
89+
90+
it('should render cell change in a new row', function () {
91+
var data = createSpreadsheetData(2, 2);
92+
createHOT(data, true);
93+
data.push(["A3", "B3"]);
94+
95+
waits(100); //Object.observe is async
96+
97+
runs(function () {
98+
expect(this.$container.find('tr:eq(2) td:eq(0)').html()).toEqual('A3');
99+
data[2][0] = 'new string';
100+
});
101+
102+
waits(100); //Object.observe is async
103+
104+
runs(function () {
105+
expect(this.$container.find('tr:eq(2) td:eq(0)').html()).toEqual('new string');
106+
});
107+
});
108+
109+
it('should not render cell change when turned off (`observeChanges: false`)', function () {
110+
var data = createSpreadsheetData(2, 2);
111+
createHOT(data, false);
112+
data[0][0] = 'new string';
113+
114+
waits(100); //Object.observe is async
115+
116+
runs(function () {
117+
expect(this.$container.find('td:eq(0)').html()).toEqual('A0');
118+
});
119+
});
120+
});

0 commit comments

Comments
 (0)
This repository has been archived.