aboutsummaryrefslogtreecommitdiff
path: root/contexts/data/lib/closure-library/closure/goog/editor/plugins/abstractbubbleplugin.js
blob: d02556db106ea8bff93a38e97e6b18b3693e30c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Base class for bubble plugins.
 */

goog.provide('goog.editor.plugins.AbstractBubblePlugin');

goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.Range');
goog.require('goog.dom.TagName');
goog.require('goog.editor.Plugin');
goog.require('goog.editor.style');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.functions');
goog.require('goog.string.Unicode');
goog.require('goog.ui.Component.EventType');
goog.require('goog.ui.editor.Bubble');
goog.require('goog.userAgent');



/**
 * Base class for bubble plugins. This is used for to connect user behavior
 * in the editor to a goog.ui.editor.Bubble UI element that allows
 * the user to modify the properties of an element on their page (e.g. the alt
 * text of an image tag).
 *
 * Subclasses should override the abstract method getBubbleTargetFromSelection()
 * with code to determine if the current selection should activate the bubble
 * type. The other abstract method createBubbleContents() should be overriden
 * with code to create the inside markup of the bubble.  The base class creates
 * the rest of the bubble.
 *
 * @constructor
 * @extends {goog.editor.Plugin}
 */
goog.editor.plugins.AbstractBubblePlugin = function() {
  goog.base(this);

  /**
   * Place to register events the plugin listens to.
   * @type {goog.events.EventHandler}
   * @protected
   */
  this.eventRegister = new goog.events.EventHandler(this);
};
goog.inherits(goog.editor.plugins.AbstractBubblePlugin, goog.editor.Plugin);


/**
 * The css class name of option link elements.
 * @type {string}
 * @private
 */
goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_ =
    goog.getCssName('tr_option-link');


/**
 * The css class name of link elements.
 * @type {string}
 * @private
 */
goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_ =
    goog.getCssName('tr_bubble_link');


/**
 * The constant string used to separate option links.
 * @type {string}
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING =
    goog.string.Unicode.NBSP + '-' + goog.string.Unicode.NBSP;


/**
 * Default factory function for creating a bubble UI component.
 * @param {!Element} parent The parent element for the bubble.
 * @param {number} zIndex The z index to draw the bubble at.
 * @return {!goog.ui.editor.Bubble} The new bubble component.
 * @private
 */
goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_ = function(
    parent, zIndex) {
  return new goog.ui.editor.Bubble(parent, zIndex);
};


/**
 * Factory function that creates a bubble UI component. It takes as parameters
 * the bubble parent element and the z index to draw the bubble at.
 * @type {function(!Element, number): !goog.ui.editor.Bubble}
 * @private
 */
goog.editor.plugins.AbstractBubblePlugin.bubbleFactory_ =
    goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_;


/**
 * Sets the bubble factory function.
 * @param {function(!Element, number): !goog.ui.editor.Bubble}
 *     bubbleFactory Function that creates a bubble for the given bubble parent
 *     element and z index.
 */
goog.editor.plugins.AbstractBubblePlugin.setBubbleFactory = function(
    bubbleFactory) {
  goog.editor.plugins.AbstractBubblePlugin.bubbleFactory_ = bubbleFactory;
};


/**
 * Map from field id to shared bubble object.
 * @type {Object.<goog.ui.editor.Bubble>}
 * @private
 */
goog.editor.plugins.AbstractBubblePlugin.bubbleMap_ = {};


/**
 * The optional parent of the bubble.  If null or not set, we will use the
 * application document. This is useful when you have an editor embedded in
 * a scrolling DIV.
 * @type {Element|undefined}
 * @private
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.bubbleParent_;


/**
 * The id of the panel this plugin added to the shared bubble.  Null when
 * this plugin doesn't currently have a panel in a bubble.
 * @type {string?}
 * @private
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.panelId_ = null;


/**
 * Sets the bubble parent.
 * @param {Element} bubbleParent An element where the bubble will be
 *     anchored. If null, we will use the application document. This
 *     is useful when you have an editor embedded in a scrolling div.
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.setBubbleParent = function(
    bubbleParent) {
  this.bubbleParent_ = bubbleParent;
};


/**
 * @return {goog.dom.DomHelper} The dom helper for the bubble window.
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleDom = function() {
  return this.dom_;
};


/** @override */
goog.editor.plugins.AbstractBubblePlugin.prototype.getTrogClassId =
    goog.functions.constant('AbstractBubblePlugin');


/**
 * Returns the element whose properties the bubble manipulates.
 * @return {Element} The target element.
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.getTargetElement =
    function() {
  return this.targetElement_;
};


/** @override */
goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyUp = function(e) {
  // For example, when an image is selected, pressing any key overwrites
  // the image and the panel should be hidden.
  // Therefore we need to track key presses when the bubble is showing.
  if (this.isVisible()) {
    this.handleSelectionChange();
  }
  return false;
};


/**
 * Pops up a property bubble for the given selection if appropriate and closes
 * open property bubbles if no longer needed.  This should not be overridden.
 * @override
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.handleSelectionChange =
    function(opt_e, opt_target) {
  var selectedElement;
  if (opt_e) {
    selectedElement = /** @type {Element} */ (opt_e.target);
  } else if (opt_target) {
    selectedElement = /** @type {Element} */ (opt_target);
  } else {
    var range = this.getFieldObject().getRange();
    if (range) {
      var startNode = range.getStartNode();
      var endNode = range.getEndNode();
      var startOffset = range.getStartOffset();
      var endOffset = range.getEndOffset();
      // Sometimes in IE, the range will be collapsed, but think the end node
      // and start node are different (although in the same visible position).
      // In this case, favor the position IE thinks is the start node.
      if (goog.userAgent.IE && range.isCollapsed() && startNode != endNode) {
        range = goog.dom.Range.createCaret(startNode, startOffset);
      }
      if (startNode.nodeType == goog.dom.NodeType.ELEMENT &&
          startNode == endNode && startOffset == endOffset - 1) {
        var element = startNode.childNodes[startOffset];
        if (element.nodeType == goog.dom.NodeType.ELEMENT) {
          selectedElement = element;
        }
      }
    }
    selectedElement = selectedElement || range && range.getContainerElement();
  }
  return this.handleSelectionChangeInternal(selectedElement);
};


/**
 * Pops up a property bubble for the given selection if appropriate and closes
 * open property bubbles if no longer needed.
 * @param {Element?} selectedElement The selected element.
 * @return {boolean} Always false, allowing every bubble plugin to handle the
 *     event.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.
    handleSelectionChangeInternal = function(selectedElement) {
  if (selectedElement) {
    var bubbleTarget = this.getBubbleTargetFromSelection(selectedElement);
    if (bubbleTarget) {
      if (bubbleTarget != this.targetElement_ || !this.panelId_) {
        // Make sure any existing panel of the same type is closed before
        // creating a new one.
        if (this.panelId_) {
          this.closeBubble();
        }
        this.createBubble(bubbleTarget);
      }
      return false;
    }
  }

  if (this.panelId_) {
    this.closeBubble();
  }

  return false;
};


/**
 * Should be overriden by subclasses to return the bubble target element or
 * null if an element of their required type isn't found.
 * @param {Element} selectedElement The target of the selection change event or
 *     the parent container of the current entire selection.
 * @return {Element?} The HTML bubble target element or null if no element of
 *     the required type is not found.
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.
    getBubbleTargetFromSelection = goog.abstractMethod;


/** @override */
goog.editor.plugins.AbstractBubblePlugin.prototype.disable = function(field) {
  // When the field is made uneditable, dispose of the bubble.  We do this
  // because the next time the field is made editable again it may be in
  // a different document / iframe.
  if (field.isUneditable()) {
    var bubble = goog.editor.plugins.AbstractBubblePlugin.bubbleMap_[field.id];
    if (bubble) {
      bubble.dispose();
      delete goog.editor.plugins.AbstractBubblePlugin.bubbleMap_[field.id];
    }
  }
};


/**
 * @return {goog.ui.editor.Bubble} The shared bubble object for the field this
 *     plugin is registered on.  Creates it if necessary.
 * @private
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.getSharedBubble_ =
    function() {
  var bubbleParent = /** @type {!Element} */ (this.bubbleParent_ ||
      this.getFieldObject().getAppWindow().document.body);
  this.dom_ = goog.dom.getDomHelper(bubbleParent);

  var bubble = goog.editor.plugins.AbstractBubblePlugin.bubbleMap_[
      this.getFieldObject().id];
  if (!bubble) {
    bubble = goog.editor.plugins.AbstractBubblePlugin.bubbleFactory_.call(null,
        bubbleParent,
        this.getFieldObject().getBaseZindex());
    goog.editor.plugins.AbstractBubblePlugin.bubbleMap_[
        this.getFieldObject().id] = bubble;
  }
  return bubble;
};


/**
 * Creates and shows the property bubble.
 * @param {Element} targetElement The target element of the bubble.
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.createBubble = function(
    targetElement) {
  var bubble = this.getSharedBubble_();
  if (!bubble.hasPanelOfType(this.getBubbleType())) {
    this.targetElement_ = targetElement;

    this.panelId_ = bubble.addPanel(this.getBubbleType(), this.getBubbleTitle(),
        targetElement,
        goog.bind(this.createBubbleContents, this),
        this.shouldPreferBubbleAboveElement());
    this.eventRegister.listen(bubble, goog.ui.Component.EventType.HIDE,
        this.handlePanelClosed_);
    this.onShow();
  }
};


/**
 * @return {string} The type of bubble shown by this plugin.  Usually the tag
 *     name of the element this bubble targets.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleType = function() {
  return '';
};


/**
 * @return {string} The title for bubble shown by this plugin.  Defaults to no
 *     title.  Should be overridden by subclasses.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleTitle = function() {
  return '';
};


/**
 * @return {boolean} Whether the bubble should prefer placement above the
 *     target element.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.
    shouldPreferBubbleAboveElement = goog.functions.FALSE;


/**
 * Should be overriden by subclasses to add the type specific contents to the
 *     bubble.
 * @param {Element} bubbleContainer The container element of the bubble to
 *     which the contents should be added.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.createBubbleContents =
    goog.abstractMethod;


/**
 * Register the handler for the target's CLICK event.
 * @param {Element} target The event source element.
 * @param {Function} handler The event handler.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.registerClickHandler =
    function(target, handler) {
  this.eventRegister.listen(target, goog.events.EventType.CLICK, handler);
};


/**
 * Closes the bubble.
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.closeBubble = function() {
  if (this.panelId_) {
    this.getSharedBubble_().removePanel(this.panelId_);
    this.handlePanelClosed_();
  }
};


/**
 * Called after the bubble is shown. The default implementation does nothing.
 * Override it to provide your own one.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.onShow = goog.nullFunction;


/**
 * Handles when the bubble panel is closed.  Invoked when the entire bubble is
 * hidden and also directly when the panel is closed manually.
 * @private
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.handlePanelClosed_ =
    function() {
  this.targetElement_ = null;
  this.panelId_ = null;
  this.eventRegister.removeAll();
};


/**
 * @return {boolean} Whether the bubble is visible.
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.isVisible = function() {
  return !!this.panelId_;
};


/**
 * Reposition the property bubble.
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.reposition = function() {
  var bubble = this.getSharedBubble_();
  if (bubble) {
    bubble.reposition();
  }
};


/**
 * Helper method that creates option links (such as edit, test, remove)
 * @param {string} id String id for the span id.
 * @return {Element} The option link element.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkOption = function(
    id) {
  // Dash plus link are together in a span so we can hide/show them easily
  return this.dom_.createDom(goog.dom.TagName.SPAN,
      {
        id: id,
        className:
            goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_
      },
      this.dom_.createTextNode(
          goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING));
};


/**
 * Helper method that creates a link with text set to linkText and optionaly
 * wires up a listener for the CLICK event or the link.
 * @param {string} linkId The id of the link.
 * @param {string} linkText Text of the link.
 * @param {Function=} opt_onClick Optional function to call when the link is
 *     clicked.
 * @param {Element=} opt_container If specified, location to insert link. If no
 *     container is specified, the old link is removed and replaced.
 * @return {Element} The link element.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.createLink = function(
    linkId, linkText, opt_onClick, opt_container) {
  var link = this.createLinkHelper(linkId, linkText, false, opt_container);
  if (opt_onClick) {
    this.registerClickHandler(link, opt_onClick);
  }
  return link;
};


/**
 * Helper method to create a link to insert into the bubble.
 * @param {string} linkId The id of the link.
 * @param {string} linkText Text of the link.
 * @param {boolean} isAnchor Set to true to create an actual anchor tag
 *     instead of a span.  Actual links are right clickable (e.g. to open in
 *     a new window) and also update window status on hover.
 * @param {Element=} opt_container If specified, location to insert link. If no
 *     container is specified, the old link is removed and replaced.
 * @return {Element} The link element.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkHelper = function(
    linkId, linkText, isAnchor, opt_container) {
  var link = this.dom_.createDom(
      isAnchor ? goog.dom.TagName.A : goog.dom.TagName.SPAN,
      {className: goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_},
      linkText);
  this.setupLink(link, linkId, opt_container);
  goog.editor.style.makeUnselectable(link, this.eventRegister);
  return link;
};


/**
 * Inserts a link in the given container if it is specified or removes
 * the old link with this id and replaces it with the new link
 * @param {Element} link Html element to insert.
 * @param {string} linkId Id of the link.
 * @param {Element=} opt_container If specified, location to insert link.
 * @protected
 */
goog.editor.plugins.AbstractBubblePlugin.prototype.setupLink = function(
    link, linkId, opt_container) {
  if (opt_container) {
    opt_container.appendChild(link);
  } else {
    var oldLink = this.dom_.getElement(linkId);
    if (oldLink) {
      goog.dom.replaceNode(link, oldLink);
    }
  }

  link.id = linkId;
};