link.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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. * @fileoverview A utility class for managing editable links.
  16. *
  17. * @author nicksantos@google.com (Nick Santos)
  18. */
  19. goog.provide('goog.editor.Link');
  20. goog.require('goog.array');
  21. goog.require('goog.dom');
  22. goog.require('goog.dom.NodeType');
  23. goog.require('goog.dom.Range');
  24. goog.require('goog.dom.TagName');
  25. goog.require('goog.editor.BrowserFeature');
  26. goog.require('goog.editor.Command');
  27. goog.require('goog.editor.Field');
  28. goog.require('goog.editor.node');
  29. goog.require('goog.editor.range');
  30. goog.require('goog.string');
  31. goog.require('goog.string.Unicode');
  32. goog.require('goog.uri.utils');
  33. goog.require('goog.uri.utils.ComponentIndex');
  34. /**
  35. * Wrap an editable link.
  36. * @param {HTMLAnchorElement} anchor The anchor element.
  37. * @param {boolean} isNew Whether this is a new link.
  38. * @constructor
  39. * @final
  40. */
  41. goog.editor.Link = function(anchor, isNew) {
  42. /**
  43. * The link DOM element.
  44. * @type {HTMLAnchorElement}
  45. * @private
  46. */
  47. this.anchor_ = anchor;
  48. /**
  49. * Whether this link represents a link just added to the document.
  50. * @type {boolean}
  51. * @private
  52. */
  53. this.isNew_ = isNew;
  54. /**
  55. * Any extra anchors created by the browser from a selection in the same
  56. * operation that created the primary link
  57. * @type {!Array<HTMLAnchorElement>}
  58. * @private
  59. */
  60. this.extraAnchors_ = [];
  61. };
  62. /**
  63. * @return {HTMLAnchorElement} The anchor element.
  64. */
  65. goog.editor.Link.prototype.getAnchor = function() {
  66. return this.anchor_;
  67. };
  68. /**
  69. * @return {!Array<HTMLAnchorElement>} The extra anchor elements, if any,
  70. * created by the browser from a selection.
  71. */
  72. goog.editor.Link.prototype.getExtraAnchors = function() {
  73. return this.extraAnchors_;
  74. };
  75. /**
  76. * @return {string} The inner text for the anchor.
  77. */
  78. goog.editor.Link.prototype.getCurrentText = function() {
  79. if (!this.currentText_) {
  80. var anchor = this.getAnchor();
  81. var leaf = goog.editor.node.getLeftMostLeaf(anchor);
  82. if (leaf.tagName && leaf.tagName == goog.dom.TagName.IMG) {
  83. this.currentText_ = leaf.getAttribute('alt');
  84. } else {
  85. this.currentText_ = goog.dom.getRawTextContent(this.getAnchor());
  86. }
  87. }
  88. return this.currentText_;
  89. };
  90. /**
  91. * @return {boolean} Whether the link is new.
  92. */
  93. goog.editor.Link.prototype.isNew = function() {
  94. return this.isNew_;
  95. };
  96. /**
  97. * Set the url without affecting the isNew() status of the link.
  98. * @param {string} url A URL.
  99. */
  100. goog.editor.Link.prototype.initializeUrl = function(url) {
  101. this.getAnchor().href = url;
  102. };
  103. /**
  104. * Removes the link, leaving its contents in the document. Note that this
  105. * object will no longer be usable/useful after this call.
  106. */
  107. goog.editor.Link.prototype.removeLink = function() {
  108. goog.dom.flattenElement(this.anchor_);
  109. this.anchor_ = null;
  110. while (this.extraAnchors_.length) {
  111. goog.dom.flattenElement(/** @type {Element} */ (this.extraAnchors_.pop()));
  112. }
  113. };
  114. /**
  115. * Change the link.
  116. * @param {string} newText New text for the link. If the link contains all its
  117. * text in one descendent, newText will only replace the text in that
  118. * one node. Otherwise, we'll change the innerHTML of the whole
  119. * link to newText.
  120. * @param {string} newUrl A new URL.
  121. */
  122. goog.editor.Link.prototype.setTextAndUrl = function(newText, newUrl) {
  123. var anchor = this.getAnchor();
  124. anchor.href = newUrl;
  125. // If the text did not change, don't update link text.
  126. var currentText = this.getCurrentText();
  127. if (newText != currentText) {
  128. var leaf = goog.editor.node.getLeftMostLeaf(anchor);
  129. if (leaf.tagName && leaf.tagName == goog.dom.TagName.IMG) {
  130. leaf.setAttribute('alt', newText ? newText : '');
  131. } else {
  132. if (leaf.nodeType == goog.dom.NodeType.TEXT) {
  133. leaf = leaf.parentNode;
  134. }
  135. if (goog.dom.getRawTextContent(leaf) != currentText) {
  136. leaf = anchor;
  137. }
  138. goog.dom.removeChildren(leaf);
  139. var domHelper = goog.dom.getDomHelper(leaf);
  140. goog.dom.appendChild(leaf, domHelper.createTextNode(newText));
  141. }
  142. // The text changed, so force getCurrentText to recompute.
  143. this.currentText_ = null;
  144. }
  145. this.isNew_ = false;
  146. };
  147. /**
  148. * Places the cursor to the right of the anchor.
  149. * Note that this is different from goog.editor.range's placeCursorNextTo
  150. * in that it specifically handles the placement of a cursor in browsers
  151. * that trap you in links, by adding a space when necessary and placing the
  152. * cursor after that space.
  153. */
  154. goog.editor.Link.prototype.placeCursorRightOf = function() {
  155. var anchor = this.getAnchor();
  156. // If the browser gets stuck in a link if we place the cursor next to it,
  157. // we'll place the cursor after a space instead.
  158. if (goog.editor.BrowserFeature.GETS_STUCK_IN_LINKS) {
  159. var spaceNode;
  160. var nextSibling = anchor.nextSibling;
  161. // Check if there is already a space after the link. Only handle the
  162. // simple case - the next node is a text node that starts with a space.
  163. if (nextSibling && nextSibling.nodeType == goog.dom.NodeType.TEXT &&
  164. (goog.string.startsWith(nextSibling.data, goog.string.Unicode.NBSP) ||
  165. goog.string.startsWith(nextSibling.data, ' '))) {
  166. spaceNode = nextSibling;
  167. } else {
  168. // If there isn't an obvious space to use, create one after the link.
  169. var dh = goog.dom.getDomHelper(anchor);
  170. spaceNode = dh.createTextNode(goog.string.Unicode.NBSP);
  171. goog.dom.insertSiblingAfter(spaceNode, anchor);
  172. }
  173. // Move the selection after the space.
  174. var range = goog.dom.Range.createCaret(spaceNode, 1);
  175. range.select();
  176. } else {
  177. goog.editor.range.placeCursorNextTo(anchor, false);
  178. }
  179. };
  180. /**
  181. * Updates the cursor position and link bubble for this link.
  182. * @param {goog.editor.Field} field The field in which the link is created.
  183. * @param {string} url The link url.
  184. * @private
  185. */
  186. goog.editor.Link.prototype.updateLinkDisplay_ = function(field, url) {
  187. this.initializeUrl(url);
  188. this.placeCursorRightOf();
  189. field.execCommand(goog.editor.Command.UPDATE_LINK_BUBBLE);
  190. };
  191. /**
  192. * @return {string?} The modified string for the link if the link
  193. * text appears to be a valid link. Returns null if this is not
  194. * a valid link address.
  195. */
  196. goog.editor.Link.prototype.getValidLinkFromText = function() {
  197. var text = goog.string.trim(this.getCurrentText());
  198. if (goog.editor.Link.isLikelyUrl(text)) {
  199. if (text.search(/:/) < 0) {
  200. return 'http://' + goog.string.trimLeft(text);
  201. }
  202. return text;
  203. } else if (goog.editor.Link.isLikelyEmailAddress(text)) {
  204. return 'mailto:' + text;
  205. }
  206. return null;
  207. };
  208. /**
  209. * After link creation, finish creating the link depending on the type
  210. * of link being created.
  211. * @param {goog.editor.Field} field The field where this link is being created.
  212. */
  213. goog.editor.Link.prototype.finishLinkCreation = function(field) {
  214. var linkFromText = this.getValidLinkFromText();
  215. if (linkFromText) {
  216. this.updateLinkDisplay_(field, linkFromText);
  217. } else {
  218. field.execCommand(goog.editor.Command.MODAL_LINK_EDITOR, this);
  219. }
  220. };
  221. /**
  222. * Initialize a new link.
  223. * @param {HTMLAnchorElement} anchor The anchor element.
  224. * @param {string} url The initial URL.
  225. * @param {string=} opt_target The target.
  226. * @param {Array<HTMLAnchorElement>=} opt_extraAnchors Extra anchors created
  227. * by the browser when parsing a selection.
  228. * @return {!goog.editor.Link} The link.
  229. */
  230. goog.editor.Link.createNewLink = function(
  231. anchor, url, opt_target, opt_extraAnchors) {
  232. var link = new goog.editor.Link(anchor, true);
  233. link.initializeUrl(url);
  234. if (opt_target) {
  235. anchor.target = opt_target;
  236. }
  237. if (opt_extraAnchors) {
  238. link.extraAnchors_ = opt_extraAnchors;
  239. }
  240. return link;
  241. };
  242. /**
  243. * Initialize a new link using text in anchor, or empty string if there is no
  244. * likely url in the anchor.
  245. * @param {HTMLAnchorElement} anchor The anchor element with likely url content.
  246. * @param {string=} opt_target The target.
  247. * @return {!goog.editor.Link} The link.
  248. */
  249. goog.editor.Link.createNewLinkFromText = function(anchor, opt_target) {
  250. var link = new goog.editor.Link(anchor, true);
  251. var text = link.getValidLinkFromText();
  252. link.initializeUrl(text ? text : '');
  253. if (opt_target) {
  254. anchor.target = opt_target;
  255. }
  256. return link;
  257. };
  258. /**
  259. * Returns true if str could be a URL, false otherwise
  260. *
  261. * Ex: TR_Util.isLikelyUrl_("http://www.google.com") == true
  262. * TR_Util.isLikelyUrl_("www.google.com") == true
  263. *
  264. * @param {string} str String to check if it looks like a URL.
  265. * @return {boolean} Whether str could be a URL.
  266. */
  267. goog.editor.Link.isLikelyUrl = function(str) {
  268. // Whitespace means this isn't a domain.
  269. if (/\s/.test(str)) {
  270. return false;
  271. }
  272. if (goog.editor.Link.isLikelyEmailAddress(str)) {
  273. return false;
  274. }
  275. // Add a scheme if the url doesn't have one - this helps the parser.
  276. var addedScheme = false;
  277. if (!/^[^:\/?#.]+:/.test(str)) {
  278. str = 'http://' + str;
  279. addedScheme = true;
  280. }
  281. // Parse the domain.
  282. var parts = goog.uri.utils.split(str);
  283. // Relax the rules for special schemes.
  284. var scheme = parts[goog.uri.utils.ComponentIndex.SCHEME];
  285. if (goog.array.indexOf(['mailto', 'aim'], scheme) != -1) {
  286. return true;
  287. }
  288. // Require domains to contain a '.', unless the domain is fully qualified and
  289. // forbids domains from containing invalid characters.
  290. var domain = parts[goog.uri.utils.ComponentIndex.DOMAIN];
  291. if (!domain || (addedScheme && domain.indexOf('.') == -1) ||
  292. (/[^\w\d\-\u0100-\uffff.%]/.test(domain))) {
  293. return false;
  294. }
  295. // Require http and ftp paths to start with '/'.
  296. var path = parts[goog.uri.utils.ComponentIndex.PATH];
  297. return !path || path.indexOf('/') == 0;
  298. };
  299. /**
  300. * Regular expression that matches strings that could be an email address.
  301. * @type {RegExp}
  302. * @private
  303. */
  304. goog.editor.Link.LIKELY_EMAIL_ADDRESS_ = new RegExp(
  305. '^' + // Test from start of string
  306. '[\\w-]+(\\.[\\w-]+)*' + // Dot-delimited alphanumerics and dashes
  307. // (name)
  308. '\\@' + // @
  309. '([\\w-]+\\.)+' + // Alphanumerics, dashes and dots (domain)
  310. '(\\d+|\\w\\w+)$', // Domain ends in at least one number or 2 letters
  311. 'i');
  312. /**
  313. * Returns true if str could be an email address, false otherwise
  314. *
  315. * Ex: goog.editor.Link.isLikelyEmailAddress_("some word") == false
  316. * goog.editor.Link.isLikelyEmailAddress_("foo@foo.com") == true
  317. *
  318. * @param {string} str String to test for being email address.
  319. * @return {boolean} Whether "str" looks like an email address.
  320. */
  321. goog.editor.Link.isLikelyEmailAddress = function(str) {
  322. return goog.editor.Link.LIKELY_EMAIL_ADDRESS_.test(str);
  323. };
  324. /**
  325. * Determines whether or not a url is an email link.
  326. * @param {string} url A url.
  327. * @return {boolean} Whether the url is a mailto link.
  328. */
  329. goog.editor.Link.isMailto = function(url) {
  330. return !!url && goog.string.startsWith(url, 'mailto:');
  331. };