Path: blob/trunk/third_party/closure/goog/editor/link.js
2868 views
// Copyright 2007 The Closure Library Authors. All Rights Reserved.1//2// Licensed under the Apache License, Version 2.0 (the "License");3// you may not use this file except in compliance with the License.4// You may obtain a copy of the License at5//6// http://www.apache.org/licenses/LICENSE-2.07//8// Unless required by applicable law or agreed to in writing, software9// distributed under the License is distributed on an "AS-IS" BASIS,10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.11// See the License for the specific language governing permissions and12// limitations under the License.1314/**15* @fileoverview A utility class for managing editable links.16*17* @author [email protected] (Nick Santos)18*/1920goog.provide('goog.editor.Link');2122goog.require('goog.array');23goog.require('goog.dom');24goog.require('goog.dom.NodeType');25goog.require('goog.dom.Range');26goog.require('goog.dom.TagName');27goog.require('goog.editor.BrowserFeature');28goog.require('goog.editor.Command');29goog.require('goog.editor.Field');30goog.require('goog.editor.node');31goog.require('goog.editor.range');32goog.require('goog.string');33goog.require('goog.string.Unicode');34goog.require('goog.uri.utils');35goog.require('goog.uri.utils.ComponentIndex');36373839/**40* Wrap an editable link.41* @param {HTMLAnchorElement} anchor The anchor element.42* @param {boolean} isNew Whether this is a new link.43* @constructor44* @final45*/46goog.editor.Link = function(anchor, isNew) {47/**48* The link DOM element.49* @type {HTMLAnchorElement}50* @private51*/52this.anchor_ = anchor;5354/**55* Whether this link represents a link just added to the document.56* @type {boolean}57* @private58*/59this.isNew_ = isNew;606162/**63* Any extra anchors created by the browser from a selection in the same64* operation that created the primary link65* @type {!Array<HTMLAnchorElement>}66* @private67*/68this.extraAnchors_ = [];69};707172/**73* @return {HTMLAnchorElement} The anchor element.74*/75goog.editor.Link.prototype.getAnchor = function() {76return this.anchor_;77};787980/**81* @return {!Array<HTMLAnchorElement>} The extra anchor elements, if any,82* created by the browser from a selection.83*/84goog.editor.Link.prototype.getExtraAnchors = function() {85return this.extraAnchors_;86};878889/**90* @return {string} The inner text for the anchor.91*/92goog.editor.Link.prototype.getCurrentText = function() {93if (!this.currentText_) {94var anchor = this.getAnchor();9596var leaf = goog.editor.node.getLeftMostLeaf(anchor);97if (leaf.tagName && leaf.tagName == goog.dom.TagName.IMG) {98this.currentText_ = leaf.getAttribute('alt');99} else {100this.currentText_ = goog.dom.getRawTextContent(this.getAnchor());101}102}103return this.currentText_;104};105106107/**108* @return {boolean} Whether the link is new.109*/110goog.editor.Link.prototype.isNew = function() {111return this.isNew_;112};113114115/**116* Set the url without affecting the isNew() status of the link.117* @param {string} url A URL.118*/119goog.editor.Link.prototype.initializeUrl = function(url) {120this.getAnchor().href = url;121};122123124/**125* Removes the link, leaving its contents in the document. Note that this126* object will no longer be usable/useful after this call.127*/128goog.editor.Link.prototype.removeLink = function() {129goog.dom.flattenElement(this.anchor_);130this.anchor_ = null;131while (this.extraAnchors_.length) {132goog.dom.flattenElement(/** @type {Element} */ (this.extraAnchors_.pop()));133}134};135136137/**138* Change the link.139* @param {string} newText New text for the link. If the link contains all its140* text in one descendent, newText will only replace the text in that141* one node. Otherwise, we'll change the innerHTML of the whole142* link to newText.143* @param {string} newUrl A new URL.144*/145goog.editor.Link.prototype.setTextAndUrl = function(newText, newUrl) {146var anchor = this.getAnchor();147anchor.href = newUrl;148149// If the text did not change, don't update link text.150var currentText = this.getCurrentText();151if (newText != currentText) {152var leaf = goog.editor.node.getLeftMostLeaf(anchor);153154if (leaf.tagName && leaf.tagName == goog.dom.TagName.IMG) {155leaf.setAttribute('alt', newText ? newText : '');156} else {157if (leaf.nodeType == goog.dom.NodeType.TEXT) {158leaf = leaf.parentNode;159}160161if (goog.dom.getRawTextContent(leaf) != currentText) {162leaf = anchor;163}164165goog.dom.removeChildren(leaf);166var domHelper = goog.dom.getDomHelper(leaf);167goog.dom.appendChild(leaf, domHelper.createTextNode(newText));168}169170// The text changed, so force getCurrentText to recompute.171this.currentText_ = null;172}173174this.isNew_ = false;175};176177178/**179* Places the cursor to the right of the anchor.180* Note that this is different from goog.editor.range's placeCursorNextTo181* in that it specifically handles the placement of a cursor in browsers182* that trap you in links, by adding a space when necessary and placing the183* cursor after that space.184*/185goog.editor.Link.prototype.placeCursorRightOf = function() {186var anchor = this.getAnchor();187// If the browser gets stuck in a link if we place the cursor next to it,188// we'll place the cursor after a space instead.189if (goog.editor.BrowserFeature.GETS_STUCK_IN_LINKS) {190var spaceNode;191var nextSibling = anchor.nextSibling;192193// Check if there is already a space after the link. Only handle the194// simple case - the next node is a text node that starts with a space.195if (nextSibling && nextSibling.nodeType == goog.dom.NodeType.TEXT &&196(goog.string.startsWith(nextSibling.data, goog.string.Unicode.NBSP) ||197goog.string.startsWith(nextSibling.data, ' '))) {198spaceNode = nextSibling;199} else {200// If there isn't an obvious space to use, create one after the link.201var dh = goog.dom.getDomHelper(anchor);202spaceNode = dh.createTextNode(goog.string.Unicode.NBSP);203goog.dom.insertSiblingAfter(spaceNode, anchor);204}205206// Move the selection after the space.207var range = goog.dom.Range.createCaret(spaceNode, 1);208range.select();209} else {210goog.editor.range.placeCursorNextTo(anchor, false);211}212};213214215/**216* Updates the cursor position and link bubble for this link.217* @param {goog.editor.Field} field The field in which the link is created.218* @param {string} url The link url.219* @private220*/221goog.editor.Link.prototype.updateLinkDisplay_ = function(field, url) {222this.initializeUrl(url);223this.placeCursorRightOf();224field.execCommand(goog.editor.Command.UPDATE_LINK_BUBBLE);225};226227228/**229* @return {string?} The modified string for the link if the link230* text appears to be a valid link. Returns null if this is not231* a valid link address.232*/233goog.editor.Link.prototype.getValidLinkFromText = function() {234var text = goog.string.trim(this.getCurrentText());235if (goog.editor.Link.isLikelyUrl(text)) {236if (text.search(/:/) < 0) {237return 'http://' + goog.string.trimLeft(text);238}239return text;240} else if (goog.editor.Link.isLikelyEmailAddress(text)) {241return 'mailto:' + text;242}243return null;244};245246247/**248* After link creation, finish creating the link depending on the type249* of link being created.250* @param {goog.editor.Field} field The field where this link is being created.251*/252goog.editor.Link.prototype.finishLinkCreation = function(field) {253var linkFromText = this.getValidLinkFromText();254if (linkFromText) {255this.updateLinkDisplay_(field, linkFromText);256} else {257field.execCommand(goog.editor.Command.MODAL_LINK_EDITOR, this);258}259};260261262/**263* Initialize a new link.264* @param {HTMLAnchorElement} anchor The anchor element.265* @param {string} url The initial URL.266* @param {string=} opt_target The target.267* @param {Array<HTMLAnchorElement>=} opt_extraAnchors Extra anchors created268* by the browser when parsing a selection.269* @return {!goog.editor.Link} The link.270*/271goog.editor.Link.createNewLink = function(272anchor, url, opt_target, opt_extraAnchors) {273var link = new goog.editor.Link(anchor, true);274link.initializeUrl(url);275276if (opt_target) {277anchor.target = opt_target;278}279if (opt_extraAnchors) {280link.extraAnchors_ = opt_extraAnchors;281}282283return link;284};285286287/**288* Initialize a new link using text in anchor, or empty string if there is no289* likely url in the anchor.290* @param {HTMLAnchorElement} anchor The anchor element with likely url content.291* @param {string=} opt_target The target.292* @return {!goog.editor.Link} The link.293*/294goog.editor.Link.createNewLinkFromText = function(anchor, opt_target) {295var link = new goog.editor.Link(anchor, true);296var text = link.getValidLinkFromText();297link.initializeUrl(text ? text : '');298if (opt_target) {299anchor.target = opt_target;300}301return link;302};303304305/**306* Returns true if str could be a URL, false otherwise307*308* Ex: TR_Util.isLikelyUrl_("http://www.google.com") == true309* TR_Util.isLikelyUrl_("www.google.com") == true310*311* @param {string} str String to check if it looks like a URL.312* @return {boolean} Whether str could be a URL.313*/314goog.editor.Link.isLikelyUrl = function(str) {315// Whitespace means this isn't a domain.316if (/\s/.test(str)) {317return false;318}319320if (goog.editor.Link.isLikelyEmailAddress(str)) {321return false;322}323324// Add a scheme if the url doesn't have one - this helps the parser.325var addedScheme = false;326if (!/^[^:\/?#.]+:/.test(str)) {327str = 'http://' + str;328addedScheme = true;329}330331// Parse the domain.332var parts = goog.uri.utils.split(str);333334// Relax the rules for special schemes.335var scheme = parts[goog.uri.utils.ComponentIndex.SCHEME];336if (goog.array.indexOf(['mailto', 'aim'], scheme) != -1) {337return true;338}339340// Require domains to contain a '.', unless the domain is fully qualified and341// forbids domains from containing invalid characters.342var domain = parts[goog.uri.utils.ComponentIndex.DOMAIN];343if (!domain || (addedScheme && domain.indexOf('.') == -1) ||344(/[^\w\d\-\u0100-\uffff.%]/.test(domain))) {345return false;346}347348// Require http and ftp paths to start with '/'.349var path = parts[goog.uri.utils.ComponentIndex.PATH];350return !path || path.indexOf('/') == 0;351};352353354/**355* Regular expression that matches strings that could be an email address.356* @type {RegExp}357* @private358*/359goog.editor.Link.LIKELY_EMAIL_ADDRESS_ = new RegExp(360'^' + // Test from start of string361'[\\w-]+(\\.[\\w-]+)*' + // Dot-delimited alphanumerics and dashes362// (name)363'\\@' + // @364'([\\w-]+\\.)+' + // Alphanumerics, dashes and dots (domain)365'(\\d+|\\w\\w+)$', // Domain ends in at least one number or 2 letters366'i');367368369/**370* Returns true if str could be an email address, false otherwise371*372* Ex: goog.editor.Link.isLikelyEmailAddress_("some word") == false373* goog.editor.Link.isLikelyEmailAddress_("[email protected]") == true374*375* @param {string} str String to test for being email address.376* @return {boolean} Whether "str" looks like an email address.377*/378goog.editor.Link.isLikelyEmailAddress = function(str) {379return goog.editor.Link.LIKELY_EMAIL_ADDRESS_.test(str);380};381382383/**384* Determines whether or not a url is an email link.385* @param {string} url A url.386* @return {boolean} Whether the url is a mailto link.387*/388goog.editor.Link.isMailto = function(url) {389return !!url && goog.string.startsWith(url, 'mailto:');390};391392393