Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/third_party/closure/goog/editor/clicktoeditwrapper.js
2868 views
1
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
// http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS-IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
/**
16
* @fileoverview A wrapper around a goog.editor.Field
17
* that listens to mouse events on the specified un-editable field, and makes
18
* the field editable if the user clicks on it. Clients are still responsible
19
* for determining when to make the field un-editable again.
20
*
21
* Clients can still determine when the field has loaded by listening to
22
* field's load event.
23
*
24
* @author [email protected] (Nick Santos)
25
*/
26
27
goog.provide('goog.editor.ClickToEditWrapper');
28
29
goog.require('goog.Disposable');
30
goog.require('goog.dom');
31
goog.require('goog.dom.Range');
32
goog.require('goog.dom.TagName');
33
goog.require('goog.editor.BrowserFeature');
34
goog.require('goog.editor.Command');
35
goog.require('goog.editor.Field');
36
goog.require('goog.editor.range');
37
goog.require('goog.events.BrowserEvent');
38
goog.require('goog.events.EventHandler');
39
goog.require('goog.events.EventType');
40
41
42
43
/**
44
* Initialize the wrapper, and begin listening to mouse events immediately.
45
* @param {goog.editor.Field} fieldObj The editable field being wrapped.
46
* @constructor
47
* @extends {goog.Disposable}
48
*/
49
goog.editor.ClickToEditWrapper = function(fieldObj) {
50
goog.Disposable.call(this);
51
52
/**
53
* The field this wrapper interacts with.
54
* @type {goog.editor.Field}
55
* @private
56
*/
57
this.fieldObj_ = fieldObj;
58
59
/**
60
* DOM helper for the field's original element.
61
* @type {goog.dom.DomHelper}
62
* @private
63
*/
64
this.originalDomHelper_ =
65
goog.dom.getDomHelper(fieldObj.getOriginalElement());
66
67
/**
68
* @type {goog.dom.SavedCaretRange}
69
* @private
70
*/
71
this.savedCaretRange_ = null;
72
73
/**
74
* Event handler for field related events.
75
* @type {!goog.events.EventHandler<!goog.editor.ClickToEditWrapper>}
76
* @private
77
*/
78
this.fieldEventHandler_ = new goog.events.EventHandler(this);
79
80
/**
81
* Bound version of the finishMouseUp method.
82
* @type {Function}
83
* @private
84
*/
85
this.finishMouseUpBound_ = goog.bind(this.finishMouseUp_, this);
86
87
/**
88
* Event handler for mouse events.
89
* @type {!goog.events.EventHandler<!goog.editor.ClickToEditWrapper>}
90
* @private
91
*/
92
this.mouseEventHandler_ = new goog.events.EventHandler(this);
93
94
// Start listening to mouse events immediately if necessary.
95
if (!this.fieldObj_.isLoaded()) {
96
this.enterDocument();
97
}
98
99
this.fieldEventHandler_
100
.
101
// Whenever the field is made editable, we need to check if there
102
// are any carets in it, and if so, use them to render the selection.
103
listen(
104
this.fieldObj_, goog.editor.Field.EventType.LOAD,
105
this.renderSelection_)
106
.
107
// Whenever the field is made uneditable, we need to set up
108
// the click-to-edit listeners.
109
listen(
110
this.fieldObj_, goog.editor.Field.EventType.UNLOAD,
111
this.enterDocument);
112
};
113
goog.inherits(goog.editor.ClickToEditWrapper, goog.Disposable);
114
115
116
117
/** @return {goog.editor.Field} The field. */
118
goog.editor.ClickToEditWrapper.prototype.getFieldObject = function() {
119
return this.fieldObj_;
120
};
121
122
123
/** @return {goog.dom.DomHelper} The dom helper of the uneditable element. */
124
goog.editor.ClickToEditWrapper.prototype.getOriginalDomHelper = function() {
125
return this.originalDomHelper_;
126
};
127
128
129
/** @override */
130
goog.editor.ClickToEditWrapper.prototype.disposeInternal = function() {
131
goog.editor.ClickToEditWrapper.base(this, 'disposeInternal');
132
this.exitDocument();
133
134
if (this.savedCaretRange_) {
135
this.savedCaretRange_.dispose();
136
}
137
138
this.fieldEventHandler_.dispose();
139
this.mouseEventHandler_.dispose();
140
this.savedCaretRange_ = null;
141
delete this.fieldEventHandler_;
142
delete this.mouseEventHandler_;
143
};
144
145
146
/**
147
* Initialize listeners when the uneditable field is added to the document.
148
* Also sets up lorem ipsum text.
149
*/
150
goog.editor.ClickToEditWrapper.prototype.enterDocument = function() {
151
if (this.isInDocument_) {
152
return;
153
}
154
155
this.isInDocument_ = true;
156
157
this.mouseEventTriggeredLoad_ = false;
158
var field = this.fieldObj_.getOriginalElement();
159
160
// To do artificial selection preservation, we have to listen to mouseup,
161
// get the current selection, and re-select the same text in the iframe.
162
//
163
// NOTE(nicksantos): Artificial selection preservation is needed in all cases
164
// where we set the field contents by setting innerHTML. There are a few
165
// rare cases where we don't need it. But these cases are highly
166
// implementation-specific, and computationally hard to detect (bidi
167
// and ig modules both set innerHTML), so we just do it in all cases.
168
this.savedAnchorClicked_ = null;
169
this.mouseEventHandler_
170
.listen(field, goog.events.EventType.MOUSEUP, this.handleMouseUp_)
171
.listen(field, goog.events.EventType.CLICK, this.handleClick_);
172
173
// manage lorem ipsum text, if necessary
174
this.fieldObj_.execCommand(goog.editor.Command.UPDATE_LOREM);
175
};
176
177
178
/**
179
* Destroy listeners when the field is removed from the document.
180
*/
181
goog.editor.ClickToEditWrapper.prototype.exitDocument = function() {
182
this.mouseEventHandler_.removeAll();
183
this.isInDocument_ = false;
184
};
185
186
187
/**
188
* Returns the uneditable field element if the field is not yet editable
189
* (equivalent to EditableField.getOriginalElement()), and the editable DOM
190
* element if the field is currently editable (equivalent to
191
* EditableField.getElement()).
192
* @return {Element} The element containing the editable field contents.
193
*/
194
goog.editor.ClickToEditWrapper.prototype.getElement = function() {
195
return this.fieldObj_.isLoaded() ? this.fieldObj_.getElement() :
196
this.fieldObj_.getOriginalElement();
197
};
198
199
200
/**
201
* True if a mouse event should be handled, false if it should be ignored.
202
* @param {goog.events.BrowserEvent} e The mouse event.
203
* @return {boolean} Wether or not this mouse event should be handled.
204
* @private
205
*/
206
goog.editor.ClickToEditWrapper.prototype.shouldHandleMouseEvent_ = function(e) {
207
return e.isButton(goog.events.BrowserEvent.MouseButton.LEFT) &&
208
!(e.shiftKey || e.ctrlKey || e.altKey || e.metaKey);
209
};
210
211
212
/**
213
* Handle mouse click events on the field.
214
* @param {goog.events.BrowserEvent} e The click event.
215
* @private
216
*/
217
goog.editor.ClickToEditWrapper.prototype.handleClick_ = function(e) {
218
// If the user clicked on a link in an uneditable field,
219
// we want to cancel the click.
220
var anchorAncestor = goog.dom.getAncestorByTagNameAndClass(
221
/** @type {Node} */ (e.target), goog.dom.TagName.A);
222
if (anchorAncestor) {
223
e.preventDefault();
224
225
if (!goog.editor.BrowserFeature.HAS_ACTIVE_ELEMENT) {
226
this.savedAnchorClicked_ = anchorAncestor;
227
}
228
}
229
};
230
231
232
/**
233
* Handle a mouse up event on the field.
234
* @param {goog.events.BrowserEvent} e The mouseup event.
235
* @private
236
*/
237
goog.editor.ClickToEditWrapper.prototype.handleMouseUp_ = function(e) {
238
// Only respond to the left mouse button.
239
if (this.shouldHandleMouseEvent_(e)) {
240
// We need to get the selection when the user mouses up, but the
241
// selection doesn't actually change until after the mouseup event has
242
// propagated. So we need to do this asynchronously.
243
this.originalDomHelper_.getWindow().setTimeout(this.finishMouseUpBound_, 0);
244
}
245
};
246
247
248
/**
249
* A helper function for handleMouseUp_ -- does the actual work
250
* when the event is finished propagating.
251
* @private
252
*/
253
goog.editor.ClickToEditWrapper.prototype.finishMouseUp_ = function() {
254
// Make sure that the field is still not editable.
255
if (!this.fieldObj_.isLoaded()) {
256
if (this.savedCaretRange_) {
257
this.savedCaretRange_.dispose();
258
this.savedCaretRange_ = null;
259
}
260
261
if (!this.fieldObj_.queryCommandValue(goog.editor.Command.USING_LOREM)) {
262
// We need carets (blank span nodes) to maintain the selection when
263
// the html is copied into an iframe. However, because our code
264
// clears the selection to make the behavior consistent, we need to do
265
// this even when we're not using an iframe.
266
this.insertCarets_();
267
}
268
269
this.ensureFieldEditable_();
270
}
271
272
this.exitDocument();
273
this.savedAnchorClicked_ = null;
274
};
275
276
277
/**
278
* Ensure that the field is editable. If the field is not editable,
279
* make it so, and record the fact that it was done by a user mouse event.
280
* @private
281
*/
282
goog.editor.ClickToEditWrapper.prototype.ensureFieldEditable_ = function() {
283
if (!this.fieldObj_.isLoaded()) {
284
this.mouseEventTriggeredLoad_ = true;
285
this.makeFieldEditable(this.fieldObj_);
286
}
287
};
288
289
290
/**
291
* Once the field has loaded in an iframe, re-create the selection
292
* as marked by the carets.
293
* @private
294
*/
295
goog.editor.ClickToEditWrapper.prototype.renderSelection_ = function() {
296
if (this.savedCaretRange_) {
297
// Make sure that the restoration document is inside the iframe
298
// if we're using one.
299
this.savedCaretRange_.setRestorationDocument(
300
this.fieldObj_.getEditableDomHelper().getDocument());
301
302
var startCaret = this.savedCaretRange_.getCaret(true);
303
var endCaret = this.savedCaretRange_.getCaret(false);
304
var hasCarets = startCaret && endCaret;
305
}
306
307
// There are two reasons why we might want to focus the field:
308
// 1) makeFieldEditable was triggered by the click-to-edit wrapper.
309
// In this case, the mouse event should have triggered a focus, but
310
// the editor might have taken the focus away to create lorem ipsum
311
// text or create an iframe for the field. So we make sure the focus
312
// is restored.
313
// 2) somebody placed carets, and we need to select those carets. The field
314
// needs focus to ensure that the selection appears.
315
if (this.mouseEventTriggeredLoad_ || hasCarets) {
316
this.focusOnFieldObj(this.fieldObj_);
317
}
318
319
if (hasCarets) {
320
this.savedCaretRange_.restore();
321
this.fieldObj_.dispatchSelectionChangeEvent();
322
323
// NOTE(nicksantos): Bubbles aren't actually enabled until the end
324
// if the load sequence, so if the user clicked on a link, the bubble
325
// will not pop up.
326
}
327
328
if (this.savedCaretRange_) {
329
this.savedCaretRange_.dispose();
330
this.savedCaretRange_ = null;
331
}
332
333
this.mouseEventTriggeredLoad_ = false;
334
};
335
336
337
/**
338
* Focus on the field object.
339
* @param {goog.editor.Field} field The field to focus.
340
* @protected
341
*/
342
goog.editor.ClickToEditWrapper.prototype.focusOnFieldObj = function(field) {
343
field.focusAndPlaceCursorAtStart();
344
};
345
346
347
/**
348
* Make the field object editable.
349
* @param {goog.editor.Field} field The field to make editable.
350
* @protected
351
*/
352
goog.editor.ClickToEditWrapper.prototype.makeFieldEditable = function(field) {
353
field.makeEditable();
354
};
355
356
357
//================================================================
358
// Caret-handling methods
359
360
361
/**
362
* Gets a saved caret range for the given range.
363
* @param {goog.dom.AbstractRange} range A range wrapper.
364
* @return {goog.dom.SavedCaretRange} The range, saved with carets, or null
365
* if the range wrapper was null.
366
* @private
367
*/
368
goog.editor.ClickToEditWrapper.createCaretRange_ = function(range) {
369
return range && goog.editor.range.saveUsingNormalizedCarets(range);
370
};
371
372
373
/**
374
* Inserts the carets, given the current selection.
375
*
376
* Note that for all practical purposes, a cursor position is just
377
* a selection with the start and end at the same point.
378
* @private
379
*/
380
goog.editor.ClickToEditWrapper.prototype.insertCarets_ = function() {
381
var fieldElement = this.fieldObj_.getOriginalElement();
382
383
this.savedCaretRange_ = null;
384
var originalWindow = this.originalDomHelper_.getWindow();
385
if (goog.dom.Range.hasSelection(originalWindow)) {
386
var range = goog.dom.Range.createFromWindow(originalWindow);
387
range = range && goog.editor.range.narrow(range, fieldElement);
388
this.savedCaretRange_ =
389
goog.editor.ClickToEditWrapper.createCaretRange_(range);
390
}
391
392
if (!this.savedCaretRange_) {
393
// We couldn't figure out where to put the carets.
394
// But in FF2/IE6+, this could mean that the user clicked on a
395
// 'special' node, (e.g., a link or an unselectable item). So the
396
// selection appears to be null or the full page, even though the user did
397
// click on something. In IE, we can determine the real selection via
398
// document.activeElement. In FF, we have to be more hacky.
399
var specialNodeClicked;
400
if (goog.editor.BrowserFeature.HAS_ACTIVE_ELEMENT) {
401
specialNodeClicked =
402
goog.dom.getActiveElement(this.originalDomHelper_.getDocument());
403
} else {
404
specialNodeClicked = this.savedAnchorClicked_;
405
}
406
407
var isFieldElement = function(node) { return node == fieldElement; };
408
if (specialNodeClicked &&
409
goog.dom.getAncestor(specialNodeClicked, isFieldElement, true)) {
410
// Insert the cursor at the beginning of the active element to be
411
// consistent with the behavior in FF1.5, where clicking on a
412
// link makes the current selection equal to the cursor position
413
// directly before that link.
414
//
415
// TODO(nicksantos): Is there a way to more accurately place the cursor?
416
this.savedCaretRange_ = goog.editor.ClickToEditWrapper.createCaretRange_(
417
goog.dom.Range.createFromNodes(
418
specialNodeClicked, 0, specialNodeClicked, 0));
419
}
420
}
421
};
422
423