abstractbubbleplugin.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. // Copyright 2005 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. * @fileoverview Base class for bubble plugins.
  16. * @author robbyw@google.com (Robby Walker)
  17. */
  18. goog.provide('goog.editor.plugins.AbstractBubblePlugin');
  19. goog.require('goog.array');
  20. goog.require('goog.dom');
  21. goog.require('goog.dom.NodeType');
  22. goog.require('goog.dom.Range');
  23. goog.require('goog.dom.TagName');
  24. goog.require('goog.dom.classlist');
  25. goog.require('goog.editor.Plugin');
  26. goog.require('goog.editor.style');
  27. goog.require('goog.events');
  28. goog.require('goog.events.EventHandler');
  29. goog.require('goog.events.EventType');
  30. goog.require('goog.events.KeyCodes');
  31. goog.require('goog.events.actionEventWrapper');
  32. goog.require('goog.functions');
  33. goog.require('goog.string.Unicode');
  34. goog.require('goog.ui.Component');
  35. goog.require('goog.ui.editor.Bubble');
  36. goog.require('goog.userAgent');
  37. /**
  38. * Base class for bubble plugins. This is used for to connect user behavior
  39. * in the editor to a goog.ui.editor.Bubble UI element that allows
  40. * the user to modify the properties of an element on their page (e.g. the alt
  41. * text of an image tag).
  42. *
  43. * Subclasses should override the abstract method getBubbleTargetFromSelection()
  44. * with code to determine if the current selection should activate the bubble
  45. * type. The other abstract method createBubbleContents() should be overriden
  46. * with code to create the inside markup of the bubble. The base class creates
  47. * the rest of the bubble.
  48. *
  49. * @constructor
  50. * @extends {goog.editor.Plugin}
  51. */
  52. goog.editor.plugins.AbstractBubblePlugin = function() {
  53. goog.editor.plugins.AbstractBubblePlugin.base(this, 'constructor');
  54. /**
  55. * Place to register events the plugin listens to.
  56. * @type {goog.events.EventHandler<
  57. * !goog.editor.plugins.AbstractBubblePlugin>}
  58. * @protected
  59. */
  60. this.eventRegister = new goog.events.EventHandler(this);
  61. /**
  62. * Instance factory function that creates a bubble UI component. If set to a
  63. * non-null value, this function will be used to create a bubble instead of
  64. * the global factory function. It takes as parameters the bubble parent
  65. * element and the z index to draw the bubble at.
  66. * @type {?function(!Element, number): !goog.ui.editor.Bubble}
  67. * @private
  68. */
  69. this.bubbleFactory_ = null;
  70. };
  71. goog.inherits(goog.editor.plugins.AbstractBubblePlugin, goog.editor.Plugin);
  72. /**
  73. * The css class name of option link elements.
  74. * @type {string}
  75. * @private
  76. */
  77. goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_ =
  78. goog.getCssName('tr_option-link');
  79. /**
  80. * The css class name of link elements.
  81. * @type {string}
  82. * @private
  83. */
  84. goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_ =
  85. goog.getCssName('tr_bubble_link');
  86. /**
  87. * A class name to mark elements that should be reachable by keyboard tabbing.
  88. * @type {string}
  89. * @private
  90. */
  91. goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_ =
  92. goog.getCssName('tr_bubble_tabbable');
  93. /**
  94. * The constant string used to separate option links.
  95. * @type {string}
  96. * @protected
  97. */
  98. goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING =
  99. goog.string.Unicode.NBSP + '-' + goog.string.Unicode.NBSP;
  100. /**
  101. * Default factory function for creating a bubble UI component.
  102. * @param {!Element} parent The parent element for the bubble.
  103. * @param {number} zIndex The z index to draw the bubble at.
  104. * @return {!goog.ui.editor.Bubble} The new bubble component.
  105. * @private
  106. */
  107. goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_ = function(
  108. parent, zIndex) {
  109. return new goog.ui.editor.Bubble(parent, zIndex);
  110. };
  111. /**
  112. * Global factory function that creates a bubble UI component. It takes as
  113. * parameters the bubble parent element and the z index to draw the bubble at.
  114. * @type {function(!Element, number): !goog.ui.editor.Bubble}
  115. * @private
  116. */
  117. goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_ =
  118. goog.editor.plugins.AbstractBubblePlugin.defaultBubbleFactory_;
  119. /**
  120. * Sets the global bubble factory function.
  121. * @param {function(!Element, number): !goog.ui.editor.Bubble}
  122. * bubbleFactory Function that creates a bubble for the given bubble parent
  123. * element and z index.
  124. */
  125. goog.editor.plugins.AbstractBubblePlugin.setBubbleFactory = function(
  126. bubbleFactory) {
  127. goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_ = bubbleFactory;
  128. };
  129. /**
  130. * Map from field id to shared bubble object.
  131. * @type {!Object<goog.ui.editor.Bubble>}
  132. * @private
  133. */
  134. goog.editor.plugins.AbstractBubblePlugin.bubbleMap_ = {};
  135. /**
  136. * The optional parent of the bubble. If null or not set, we will use the
  137. * application document. This is useful when you have an editor embedded in
  138. * a scrolling DIV.
  139. * @type {Element|undefined}
  140. * @private
  141. */
  142. goog.editor.plugins.AbstractBubblePlugin.prototype.bubbleParent_;
  143. /**
  144. * The id of the panel this plugin added to the shared bubble. Null when
  145. * this plugin doesn't currently have a panel in a bubble.
  146. * @type {string?}
  147. * @private
  148. */
  149. goog.editor.plugins.AbstractBubblePlugin.prototype.panelId_ = null;
  150. /**
  151. * Whether this bubble should support tabbing through elements. False
  152. * by default.
  153. * @type {boolean}
  154. * @private
  155. */
  156. goog.editor.plugins.AbstractBubblePlugin.prototype.keyboardNavigationEnabled_ =
  157. false;
  158. /**
  159. * Sets the instance bubble factory function. If set to a non-null value, this
  160. * function will be used to create a bubble instead of the global factory
  161. * function.
  162. * @param {?function(!Element, number): !goog.ui.editor.Bubble} bubbleFactory
  163. * Function that creates a bubble for the given bubble parent element and z
  164. * index. Null to reset the factory function.
  165. */
  166. goog.editor.plugins.AbstractBubblePlugin.prototype.setBubbleFactory = function(
  167. bubbleFactory) {
  168. this.bubbleFactory_ = bubbleFactory;
  169. };
  170. /**
  171. * Sets whether the bubble should support tabbing through elements.
  172. * @param {boolean} keyboardNavigationEnabled
  173. */
  174. goog.editor.plugins.AbstractBubblePlugin.prototype.enableKeyboardNavigation =
  175. function(keyboardNavigationEnabled) {
  176. this.keyboardNavigationEnabled_ = keyboardNavigationEnabled;
  177. };
  178. /**
  179. * Sets the bubble parent.
  180. * @param {Element} bubbleParent An element where the bubble will be
  181. * anchored. If null, we will use the application document. This
  182. * is useful when you have an editor embedded in a scrolling div.
  183. */
  184. goog.editor.plugins.AbstractBubblePlugin.prototype.setBubbleParent = function(
  185. bubbleParent) {
  186. this.bubbleParent_ = bubbleParent;
  187. };
  188. /**
  189. * Returns the bubble map. Subclasses may override to use a separate map.
  190. * @return {!Object<goog.ui.editor.Bubble>}
  191. * @protected
  192. */
  193. goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleMap = function() {
  194. return goog.editor.plugins.AbstractBubblePlugin.bubbleMap_;
  195. };
  196. /**
  197. * @return {goog.dom.DomHelper} The dom helper for the bubble window.
  198. */
  199. goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleDom = function() {
  200. return this.dom_;
  201. };
  202. /** @override */
  203. goog.editor.plugins.AbstractBubblePlugin.prototype.getTrogClassId =
  204. goog.functions.constant('AbstractBubblePlugin');
  205. /**
  206. * Returns the element whose properties the bubble manipulates.
  207. * @return {Element} The target element.
  208. */
  209. goog.editor.plugins.AbstractBubblePlugin.prototype.getTargetElement =
  210. function() {
  211. return this.targetElement_;
  212. };
  213. /** @override */
  214. goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyUp = function(e) {
  215. // For example, when an image is selected, pressing any key overwrites
  216. // the image and the panel should be hidden.
  217. // Therefore we need to track key presses when the bubble is showing.
  218. if (this.isVisible()) {
  219. this.handleSelectionChange();
  220. }
  221. return false;
  222. };
  223. /**
  224. * Pops up a property bubble for the given selection if appropriate and closes
  225. * open property bubbles if no longer needed. This should not be overridden.
  226. * @override
  227. */
  228. goog.editor.plugins.AbstractBubblePlugin.prototype.handleSelectionChange =
  229. function(opt_e, opt_target) {
  230. var selectedElement;
  231. if (opt_e) {
  232. selectedElement = /** @type {Element} */ (opt_e.target);
  233. } else if (opt_target) {
  234. selectedElement = /** @type {Element} */ (opt_target);
  235. } else {
  236. var range = this.getFieldObject().getRange();
  237. if (range) {
  238. var startNode = range.getStartNode();
  239. var endNode = range.getEndNode();
  240. var startOffset = range.getStartOffset();
  241. var endOffset = range.getEndOffset();
  242. // Sometimes in IE, the range will be collapsed, but think the end node
  243. // and start node are different (although in the same visible position).
  244. // In this case, favor the position IE thinks is the start node.
  245. if (goog.userAgent.IE && range.isCollapsed() && startNode != endNode) {
  246. range = goog.dom.Range.createCaret(startNode, startOffset);
  247. }
  248. if (startNode.nodeType == goog.dom.NodeType.ELEMENT &&
  249. startNode == endNode && startOffset == endOffset - 1) {
  250. var element = startNode.childNodes[startOffset];
  251. if (element.nodeType == goog.dom.NodeType.ELEMENT) {
  252. selectedElement = /** @type {!Element} */ (element);
  253. }
  254. }
  255. }
  256. selectedElement = selectedElement || range && range.getContainerElement();
  257. }
  258. return this.handleSelectionChangeInternal(selectedElement);
  259. };
  260. /**
  261. * Pops up a property bubble for the given selection if appropriate and closes
  262. * open property bubbles if no longer needed.
  263. * @param {Element?} selectedElement The selected element.
  264. * @return {boolean} Always false, allowing every bubble plugin to handle the
  265. * event.
  266. * @protected
  267. */
  268. goog.editor.plugins.AbstractBubblePlugin.prototype
  269. .handleSelectionChangeInternal = function(selectedElement) {
  270. if (selectedElement) {
  271. var bubbleTarget = this.getBubbleTargetFromSelection(selectedElement);
  272. if (bubbleTarget) {
  273. if (bubbleTarget != this.targetElement_ || !this.panelId_) {
  274. // Make sure any existing panel of the same type is closed before
  275. // creating a new one.
  276. if (this.panelId_) {
  277. this.closeBubble();
  278. }
  279. this.createBubble(bubbleTarget);
  280. }
  281. return false;
  282. }
  283. }
  284. if (this.panelId_) {
  285. this.closeBubble();
  286. }
  287. return false;
  288. };
  289. /**
  290. * Should be overriden by subclasses to return the bubble target element or
  291. * null if an element of their required type isn't found.
  292. * @param {Element} selectedElement The target of the selection change event or
  293. * the parent container of the current entire selection.
  294. * @return {Element?} The HTML bubble target element or null if no element of
  295. * the required type is not found.
  296. */
  297. goog.editor.plugins.AbstractBubblePlugin.prototype
  298. .getBubbleTargetFromSelection = goog.abstractMethod;
  299. /** @override */
  300. goog.editor.plugins.AbstractBubblePlugin.prototype.disable = function(field) {
  301. // When the field is made uneditable, dispose of the bubble. We do this
  302. // because the next time the field is made editable again it may be in
  303. // a different document / iframe.
  304. if (field.isUneditable()) {
  305. var bubbleMap = this.getBubbleMap();
  306. var bubble = bubbleMap[field.id];
  307. if (bubble) {
  308. if (field == this.getFieldObject()) {
  309. this.closeBubble();
  310. }
  311. bubble.dispose();
  312. delete bubbleMap[field.id];
  313. }
  314. }
  315. };
  316. /**
  317. * @return {!goog.ui.editor.Bubble} The shared bubble object for the field this
  318. * plugin is registered on. Creates it if necessary.
  319. * @private
  320. */
  321. goog.editor.plugins.AbstractBubblePlugin.prototype.getSharedBubble_ =
  322. function() {
  323. var bubbleParent = /** @type {!Element} */ (
  324. this.bubbleParent_ || this.getFieldObject().getAppWindow().document.body);
  325. this.dom_ = goog.dom.getDomHelper(bubbleParent);
  326. var bubbleMap = this.getBubbleMap();
  327. var bubble = bubbleMap[this.getFieldObject().id];
  328. if (!bubble) {
  329. var factory = this.bubbleFactory_ ||
  330. goog.editor.plugins.AbstractBubblePlugin.globalBubbleFactory_;
  331. bubble =
  332. factory.call(null, bubbleParent, this.getFieldObject().getBaseZindex());
  333. bubbleMap[this.getFieldObject().id] = bubble;
  334. }
  335. return bubble;
  336. };
  337. /**
  338. * Creates and shows the property bubble.
  339. * @param {Element} targetElement The target element of the bubble.
  340. */
  341. goog.editor.plugins.AbstractBubblePlugin.prototype.createBubble = function(
  342. targetElement) {
  343. var bubble = this.getSharedBubble_();
  344. if (!bubble.hasPanelOfType(this.getBubbleType())) {
  345. this.targetElement_ = targetElement;
  346. this.panelId_ = bubble.addPanel(
  347. this.getBubbleType(), this.getBubbleTitle(), targetElement,
  348. goog.bind(this.createBubbleContents, this),
  349. this.shouldPreferBubbleAboveElement());
  350. this.eventRegister.listen(
  351. bubble, goog.ui.Component.EventType.HIDE, this.handlePanelClosed_);
  352. this.onShow();
  353. if (this.keyboardNavigationEnabled_) {
  354. this.eventRegister.listen(
  355. bubble.getContentElement(), goog.events.EventType.KEYDOWN,
  356. this.onBubbleKey_);
  357. }
  358. }
  359. };
  360. /**
  361. * @return {string} The type of bubble shown by this plugin. Usually the tag
  362. * name of the element this bubble targets.
  363. * @protected
  364. */
  365. goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleType = function() {
  366. return '';
  367. };
  368. /**
  369. * @return {string} The title for bubble shown by this plugin. Defaults to no
  370. * title. Should be overridden by subclasses.
  371. * @protected
  372. */
  373. goog.editor.plugins.AbstractBubblePlugin.prototype.getBubbleTitle = function() {
  374. return '';
  375. };
  376. /**
  377. * @return {boolean} Whether the bubble should prefer placement above the
  378. * target element.
  379. * @protected
  380. */
  381. goog.editor.plugins.AbstractBubblePlugin.prototype
  382. .shouldPreferBubbleAboveElement = goog.functions.FALSE;
  383. /**
  384. * Should be overriden by subclasses to add the type specific contents to the
  385. * bubble.
  386. * @param {Element} bubbleContainer The container element of the bubble to
  387. * which the contents should be added.
  388. * @protected
  389. */
  390. goog.editor.plugins.AbstractBubblePlugin.prototype.createBubbleContents =
  391. goog.abstractMethod;
  392. /**
  393. * Register the handler for the target's CLICK event.
  394. * @param {Element} target The event source element.
  395. * @param {Function} handler The event handler.
  396. * @protected
  397. * @deprecated Use goog.editor.plugins.AbstractBubblePlugin.
  398. * registerActionHandler to register click and enter events.
  399. */
  400. goog.editor.plugins.AbstractBubblePlugin.prototype.registerClickHandler =
  401. function(target, handler) {
  402. this.registerActionHandler(target, handler);
  403. };
  404. /**
  405. * Register the handler for the target's CLICK and ENTER key events.
  406. * @param {Element} target The event source element.
  407. * @param {Function} handler The event handler.
  408. * @protected
  409. */
  410. goog.editor.plugins.AbstractBubblePlugin.prototype.registerActionHandler =
  411. function(target, handler) {
  412. this.eventRegister.listenWithWrapper(
  413. target, goog.events.actionEventWrapper, handler);
  414. };
  415. /**
  416. * Closes the bubble.
  417. */
  418. goog.editor.plugins.AbstractBubblePlugin.prototype.closeBubble = function() {
  419. if (this.panelId_) {
  420. this.getSharedBubble_().removePanel(this.panelId_);
  421. this.handlePanelClosed_();
  422. }
  423. };
  424. /**
  425. * Called after the bubble is shown. The default implementation does nothing.
  426. * Override it to provide your own one.
  427. * @protected
  428. */
  429. goog.editor.plugins.AbstractBubblePlugin.prototype.onShow = goog.nullFunction;
  430. /**
  431. * Called when the bubble is closed or hidden. The default implementation does
  432. * nothing.
  433. * @protected
  434. */
  435. goog.editor.plugins.AbstractBubblePlugin.prototype.cleanOnBubbleClose =
  436. goog.nullFunction;
  437. /**
  438. * Handles when the bubble panel is closed. Invoked when the entire bubble is
  439. * hidden and also directly when the panel is closed manually.
  440. * @private
  441. */
  442. goog.editor.plugins.AbstractBubblePlugin.prototype.handlePanelClosed_ =
  443. function() {
  444. this.targetElement_ = null;
  445. this.panelId_ = null;
  446. this.eventRegister.removeAll();
  447. this.cleanOnBubbleClose();
  448. };
  449. /**
  450. * In case the keyboard navigation is enabled, this will set focus on the first
  451. * tabbable element in the bubble when TAB is clicked.
  452. * @override
  453. */
  454. goog.editor.plugins.AbstractBubblePlugin.prototype.handleKeyDown = function(e) {
  455. if (this.keyboardNavigationEnabled_ && this.isVisible() &&
  456. e.keyCode == goog.events.KeyCodes.TAB && !e.shiftKey) {
  457. var bubbleEl = this.getSharedBubble_().getContentElement();
  458. var tabbable = goog.dom.getElementByClass(
  459. goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_, bubbleEl);
  460. if (tabbable) {
  461. tabbable.focus();
  462. e.preventDefault();
  463. return true;
  464. }
  465. }
  466. return false;
  467. };
  468. /**
  469. * Handles a key event on the bubble. This ensures that the focus loops through
  470. * the tabbable elements found in the bubble and then the focus is got by the
  471. * field element.
  472. * @param {goog.events.BrowserEvent} e The event.
  473. * @private
  474. */
  475. goog.editor.plugins.AbstractBubblePlugin.prototype.onBubbleKey_ = function(e) {
  476. if (this.isVisible() && e.keyCode == goog.events.KeyCodes.TAB) {
  477. var bubbleEl = this.getSharedBubble_().getContentElement();
  478. var tabbables = goog.dom.getElementsByClass(
  479. goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_, bubbleEl);
  480. var tabbable = e.shiftKey ? tabbables[0] : goog.array.peek(tabbables);
  481. var tabbingOutOfBubble = tabbable == e.target;
  482. if (tabbingOutOfBubble) {
  483. this.getFieldObject().focus();
  484. e.preventDefault();
  485. }
  486. }
  487. };
  488. /**
  489. * @return {boolean} Whether the bubble is visible.
  490. */
  491. goog.editor.plugins.AbstractBubblePlugin.prototype.isVisible = function() {
  492. return !!this.panelId_;
  493. };
  494. /**
  495. * Reposition the property bubble.
  496. */
  497. goog.editor.plugins.AbstractBubblePlugin.prototype.reposition = function() {
  498. var bubble = this.getSharedBubble_();
  499. if (bubble) {
  500. bubble.reposition();
  501. }
  502. };
  503. /**
  504. * Helper method that creates option links (such as edit, test, remove)
  505. * @param {string} id String id for the span id.
  506. * @return {Element} The option link element.
  507. * @protected
  508. */
  509. goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkOption = function(
  510. id) {
  511. // Dash plus link are together in a span so we can hide/show them easily
  512. return this.dom_.createDom(
  513. goog.dom.TagName.SPAN, {
  514. id: id,
  515. className:
  516. goog.editor.plugins.AbstractBubblePlugin.OPTION_LINK_CLASSNAME_
  517. },
  518. this.dom_.createTextNode(
  519. goog.editor.plugins.AbstractBubblePlugin.DASH_NBSP_STRING));
  520. };
  521. /**
  522. * Helper method that creates a link with text set to linkText and optionally
  523. * wires up a listener for the CLICK event or the link. The link is navigable by
  524. * tabs if {@code enableKeyboardNavigation(true)} was called.
  525. * @param {string} linkId The id of the link.
  526. * @param {string} linkText Text of the link.
  527. * @param {Function=} opt_onClick Optional function to call when the link is
  528. * clicked.
  529. * @param {Element=} opt_container If specified, location to insert link. If no
  530. * container is specified, the old link is removed and replaced.
  531. * @return {Element} The link element.
  532. * @protected
  533. */
  534. goog.editor.plugins.AbstractBubblePlugin.prototype.createLink = function(
  535. linkId, linkText, opt_onClick, opt_container) {
  536. var link = this.createLinkHelper(linkId, linkText, false, opt_container);
  537. if (opt_onClick) {
  538. this.registerActionHandler(link, opt_onClick);
  539. }
  540. return link;
  541. };
  542. /**
  543. * Helper method to create a link to insert into the bubble. The link is
  544. * navigable by tabs if {@code enableKeyboardNavigation(true)} was called.
  545. * @param {string} linkId The id of the link.
  546. * @param {string} linkText Text of the link.
  547. * @param {boolean} isAnchor Set to true to create an actual anchor tag
  548. * instead of a span. Actual links are right clickable (e.g. to open in
  549. * a new window) and also update window status on hover.
  550. * @param {Element=} opt_container If specified, location to insert link. If no
  551. * container is specified, the old link is removed and replaced.
  552. * @return {Element} The link element.
  553. * @protected
  554. */
  555. goog.editor.plugins.AbstractBubblePlugin.prototype.createLinkHelper = function(
  556. linkId, linkText, isAnchor, opt_container) {
  557. var link = this.dom_.createDom(
  558. isAnchor ? goog.dom.TagName.A : goog.dom.TagName.SPAN,
  559. {className: goog.editor.plugins.AbstractBubblePlugin.LINK_CLASSNAME_},
  560. linkText);
  561. if (this.keyboardNavigationEnabled_) {
  562. this.setTabbable(link);
  563. }
  564. link.setAttribute('role', 'link');
  565. this.setupLink(link, linkId, opt_container);
  566. goog.editor.style.makeUnselectable(link, this.eventRegister);
  567. return link;
  568. };
  569. /**
  570. * Makes the given element tabbable.
  571. *
  572. * <p>Elements created by createLink[Helper] are tabbable even without
  573. * calling this method. Call it for other elements if needed.
  574. *
  575. * <p>If tabindex is not already set in the element, this function sets it to 0.
  576. * You'll usually want to also call {@code enableKeyboardNavigation(true)}.
  577. *
  578. * @param {!Element} element
  579. * @protected
  580. */
  581. goog.editor.plugins.AbstractBubblePlugin.prototype.setTabbable = function(
  582. element) {
  583. if (!element.hasAttribute('tabindex')) {
  584. element.setAttribute('tabindex', 0);
  585. }
  586. goog.dom.classlist.add(
  587. element, goog.editor.plugins.AbstractBubblePlugin.TABBABLE_CLASSNAME_);
  588. };
  589. /**
  590. * Inserts a link in the given container if it is specified or removes
  591. * the old link with this id and replaces it with the new link
  592. * @param {Element} link Html element to insert.
  593. * @param {string} linkId Id of the link.
  594. * @param {Element=} opt_container If specified, location to insert link.
  595. * @protected
  596. */
  597. goog.editor.plugins.AbstractBubblePlugin.prototype.setupLink = function(
  598. link, linkId, opt_container) {
  599. if (opt_container) {
  600. opt_container.appendChild(link);
  601. } else {
  602. var oldLink = this.dom_.getElement(linkId);
  603. if (oldLink) {
  604. goog.dom.replaceNode(link, oldLink);
  605. }
  606. }
  607. link.id = linkId;
  608. };