/* link-plugin.js is part of Aloha Editor project http://aloha-editor.org * * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor. * Copyright (c) 2010-2013 Gentics Software GmbH, Vienna, Austria. * Contributors http://aloha-editor.org/contribution.php * * Aloha Editor is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or any later version. * * Aloha Editor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * As an additional permission to the GNU GPL version 2, you may distribute * non-source (e.g., minimized or compacted) forms of the Aloha-Editor * source code without the copy of the GNU GPL normally required, * provided you include this license notice and a URL through which * recipients can access the Corresponding Source. */ /* Aloha Link Plugin * ----------------- * This plugin provides an interface to allow the user to insert, edit and * remove links within an active editable. * It presents its user interface in the Toolbar, in a Sidebar panel. * Clicking on any links inside the editable activates the this plugin's * floating menu scope. */ define( [ 'aloha', 'aloha/plugin', 'aloha/ephemera', 'jquery', 'ui/port-helper-attribute-field', 'ui/ui', 'ui/scopes', 'ui/surface', 'ui/button', 'ui/toggleButton', 'i18n!link/nls/i18n', 'i18n!aloha/nls/i18n', 'aloha/console', 'PubSub', 'util/keys' ], function ( Aloha, Plugin, Ephemera, jQuery, AttributeField, Ui, Scopes, Surface, Button, ToggleButton, i18n, i18nCore, console, PubSub, Keys ) { 'use strict'; var GENTICS = window.GENTICS, pluginNamespace = 'aloha-link', oldValue = '', newValue; /** * Properties for cleaning up markup immediately after inserting new link * markup. * * Successive anchor elements are generally not merged, but an exception * needs to be made in the process of creating links: adjacent fragments of * new links are coalesced whenever possible. * * @type {object} */ var insertLinkPostCleanup = { merge: true, mergeable: function (node) { return ('aloha-new-link' === node.className && node.nextSibling && 'aloha-new-link' === node.nextSibling.className); } }; Ephemera.classes('aloha-link-pointer', 'aloha-link-text'); function setupMousePointerFix() { jQuery(document) .bind('keydown.aloha-link.pointer-fix', function (e) { // metaKey for OSX, 17 for PC (we can't check // e.ctrlKey because it's only set on keyup or // keypress, not on keydown). if (e.metaKey || Keys.getToken(e.keyCode) === 'control') { jQuery('body').addClass('aloha-link-pointer'); } }) .bind('keyup.aloha-link.pointer-fix', function (e) { if (e.metaKey || Keys.getToken(e.keyCode) === 'control') { jQuery('body').removeClass('aloha-link-pointer'); } }); } function teardownMousePointerFix() { jQuery(document).unbind('.aloha-link.pointer-fix'); } function setupMetaClickLink(editable) { editable.obj.delegate('a', 'click.aloha-link.meta-click-link', function (e) { // Use metaKey for OSX and ctrlKey for PC if (e.metaKey || e.ctrlKey) { // blur current editable. user is waiting for the link to load Aloha.activeEditable.blur(); // hack to guarantee a browser history entry window.setTimeout(function () { location.href = e.target; }, 0); e.stopPropagation(); return false; } }); } function teardownMetaClickLink(editable) { editable.obj.unbind('.aloha-link.meta-click-link'); } return Plugin.create('link', { /** * Default configuration allows links everywhere */ config: [ 'a' ], /** * The value that will automatically be set to an anchor tag's title * attribute if its href field matches the titleregex, and the editor * has not manually defined the title attribute. * * @type {string} */ title: null, /** * Regular Expression string which the field's href value will be tested * against in order to determine whether or not to set the configured * title attribute value. * * @type {string} */ titleregex: null, /** * all links that match the targetregex will get set the target * e.g. ^(?!.*aloha-editor.com).* matches all href except aloha-editor.com */ targetregex: '', /** * this target is set when either targetregex matches or not set * e.g. _blank opens all links in new window */ target: '', /** * all links that match the cssclassregex will get set the css class * e.g. ^(?!.*aloha-editor.com).* matches all href except aloha-editor.com */ cssclassregex: null, /** * this target is set when either cssclassregex matches or not set */ cssclass: '', /** * the defined object types to be used for this instance */ objectTypeFilter: [], /** * handle change on href change * called function ( obj, href, item ); */ onHrefChange: null, /** * This variable is used to ignore one selection changed event. We need * to ignore one selectionchanged event when we set our own selection. */ ignoreNextSelectionChangedEvent: false, /** * Internal update interval reference to work around an ExtJS bug */ hrefUpdateInt: null, /** * HotKeys used for special actions */ hotKey: { insertLink: i18n.t('insertLink', 'ctrl+k') }, /** * Default input value for a new link */ hrefValue: 'http://', /** * Initialize the plugin */ init: function () { var plugin = this; if ('undefined' !== typeof this.settings.title) { this.title = this.settings.title; } if ('undefined' !== typeof this.settings.titleregex) { this.titleregex = this.settings.titleregex; } if ( typeof this.settings.targetregex != 'undefined' ) { this.targetregex = this.settings.targetregex; } if ( typeof this.settings.target != 'undefined' ) { this.target = this.settings.target; } if ( typeof this.settings.cssclassregex != 'undefined' ) { this.cssclassregex = this.settings.cssclassregex; } if ( typeof this.settings.cssclass != 'undefined' ) { this.cssclass = this.settings.cssclass; } if ( typeof this.settings.objectTypeFilter != 'undefined' ) { this.objectTypeFilter = this.settings.objectTypeFilter; } if ( typeof this.settings.onHrefChange != 'undefined' ) { this.onHrefChange = this.settings.onHrefChange; } if ( typeof this.settings.hotKey != 'undefined' ) { jQuery.extend(true, this.hotKey, this.settings.hotKey); } if ( typeof this.settings.hrefValue != 'undefined' ) { this.hrefValue = this.settings.hrefValue; } this.createButtons(); this.subscribeEvents(); this.bindInteractions(); Aloha.bind('aloha-plugins-loaded', function () { plugin.initSidebar(Aloha.Sidebar.right); PubSub.pub('aloha.link.ready', { plugin: plugin }) }); }, nsSel: function () { var stringBuilder = [], prefix = pluginNamespace; jQuery.each( arguments, function () { stringBuilder.push( '.' + ( this == '' ? prefix : prefix + '-' + this ) ); } ); return jQuery.trim(stringBuilder.join(' ')); }, //Creates string with this component's namepsace prefixed the each classname nsClass: function () { var stringBuilder = [], prefix = pluginNamespace; jQuery.each( arguments, function () { stringBuilder.push( this == '' ? prefix : prefix + '-' + this ); } ); return jQuery.trim(stringBuilder.join(' ')); }, initSidebar: function ( sidebar ) { var pl = this; pl.sidebar = sidebar; sidebar.addPanel( { id : pl.nsClass( 'sidebar-panel-target' ), title : i18n.t( 'floatingmenu.tab.link' ), content : '', expanded : true, activeOn : 'a, link', onInit: function () { var that = this, content = this.setContent( '
' + '' ).content; jQuery( pl.nsSel( 'framename' ) ).live( 'keyup', function () { jQuery( that.effective ).attr( 'target', jQuery( this ).val().replace( '\"', '"' ).replace( "'", "'" ) ); } ); jQuery( pl.nsSel( 'radioTarget' ) ).live( 'change', function () { if ( jQuery( this ).val() == 'framename' ) { jQuery( pl.nsSel( 'framename' ) ).slideDown(); } else { jQuery( pl.nsSel( 'framename' ) ).slideUp().val( '' ); jQuery( that.effective ).attr( 'target', jQuery( this ).val() ); } } ); jQuery( pl.nsSel( 'linkTitle' ) ).live( 'keyup', function () { jQuery( that.effective ).attr( 'title', jQuery( this ).val().replace( '\"', '"' ).replace( "'", "'" ) ); } ); }, onActivate: function ( effective ) { var that = this; that.effective = effective; if ( jQuery( that.effective ).attr( 'target' ) != null ) { var isFramename = true; jQuery( pl.nsSel( 'framename' ) ).hide().val( '' ); jQuery( pl.nsSel( 'radioTarget' ) ).each( function () { jQuery( this ).removeAttr('checked'); if ( jQuery( this ).val() === jQuery( that.effective ).attr( 'target' ) ) { isFramename = false; jQuery( this ).attr( 'checked', 'checked' ); } } ); if ( isFramename ) { jQuery( pl.nsSel( 'radioTarget[value="framename"]' ) ).attr( 'checked', 'checked' ); jQuery( pl.nsSel( 'framename' ) ) .val( jQuery( that.effective ).attr( 'target' ) ) .show(); } } else { jQuery( pl.nsSel( 'radioTarget' ) ).first().attr( 'checked', 'checked' ); jQuery( that.effective ).attr( 'target', jQuery( pl.nsSel( 'radioTarget' ) ).first().val() ); } var that = this; that.effective = effective; jQuery( pl.nsSel( 'linkTitle' ) ).val( jQuery( that.effective ).attr( 'title' ) ); } } ); sidebar.show(); }, /** * Subscribe for events */ subscribeEvents: function () { var that = this, isEnabled = {}; var editablesCreated = 0; // add the event handler for creation of editables Aloha.bind('aloha-editable-created', function (event, editable) { var config = that.getEditableConfig(editable.obj), enabled = (jQuery.inArray('a', config) !== -1); isEnabled[editable.getId()] = enabled; if (!enabled) { return; } // enable hotkey for inserting links editable.obj.bind('keydown.aloha-link', that.hotKey.insertLink, function() { if ( that.findLinkMarkup() ) { // open the tab containing the href that.hrefField.foreground(); that.hrefField.focus(); } else { that.insertLink(true); } return false; } ); editable.obj.find('a').each(function() { that.addLinkEventHandlers(this); }); if (0 === editablesCreated++) { setupMousePointerFix(); } }); Aloha.bind('aloha-editable-destroyed', function (event, editable) { editable.obj.unbind('.aloha-link'); if (0 === --editablesCreated) { teardownMousePointerFix(); } }); Aloha.bind('aloha-editable-activated', function(event, props) { if (isEnabled[Aloha.activeEditable.getId()]) { that._formatLinkButton.show(); that._insertLinkButton.show(); } else { that._formatLinkButton.hide(); that._insertLinkButton.hide(); } setupMetaClickLink(props.editable); }); var insideLinkScope = false; Aloha.bind('aloha-selection-changed', function(event, rangeObject){ var enteredLinkScope = false; if (Aloha.activeEditable && isEnabled[Aloha.activeEditable.getId()]) { enteredLinkScope = selectionChangeHandler(that, rangeObject); // Only foreground the tab containing the href field // the first time the user enters the link scope to // avoid intefering with the user's manual tab // selection. if (enteredLinkScope && insideLinkScope !== enteredLinkScope) { that.hrefField.foreground(); } } insideLinkScope = enteredLinkScope; }); // Fixes problem: if one clicks from inside an aloha link // outside the editable and thereby deactivates the // editable, the link scope will remain active. var linkPlugin = this; Aloha.bind('aloha-editable-deactivated', function (event, props) { if (insideLinkScope) { // Leave the link scope lazily to avoid flickering // when switching between anchor element editables. setTimeout(function () { if (!insideLinkScope) { linkPlugin.toggleLinkScope(false); } }, 100); insideLinkScope = false; } teardownMetaClickLink(props.editable); }); }, /** * lets you toggle the link scope to true or false * @param show bool */ toggleLinkScope: function ( show ) { // Check before doing anything as a performance improvement. // The _isScopeActive_editableId check ensures that when // changing from a normal link in an editable to an editable // that is a link itself, the removeLinkButton will be // hidden. if (this._isScopeActive === show && Aloha.activeEditable && this._isScopeActive_editableId === Aloha.activeEditable.getId()) { return; } this._isScopeActive = show; this._isScopeActive_editableId = Aloha.activeEditable && Aloha.activeEditable.getId(); if ( show ) { this.hrefField.show(); this._insertLinkButton.hide(); // Never show the removeLinkButton when the link itself // is the editable. if (Aloha.activeEditable && Aloha.activeEditable.obj[0].nodeName === 'A') { this._removeLinkButton.hide(); } else { this._removeLinkButton.show(); } this._formatLinkButton.setState(true); Scopes.enterScope(this.name, 'link'); } else { this.hrefField.hide(); this._insertLinkButton.show(); this._removeLinkButton.hide(); this._formatLinkButton.setState(false); // The calls to enterScope and leaveScope by the link // plugin are not balanced. // When the selection is changed from one link to // another, the link scope is incremented more than // decremented, which necessitates the force=true // argument to leaveScope. Scopes.leaveScope(this.name, 'link', true); } }, /** * Add event handlers to the given link object * @param link object */ addLinkEventHandlers: function ( link ) { var that = this; // show pointer on mouse over jQuery( link ).mouseenter( function ( e ) { Aloha.Log.debug( that, 'mouse over link.' ); that.mouseOverLink = link; that.updateMousePointer(); } ); // in any case on leave show text cursor jQuery( link ).mouseleave( function ( e ) { Aloha.Log.debug( that, 'mouse left link.' ); that.mouseOverLink = null; that.updateMousePointer(); } ); // follow link on ctrl or meta + click jQuery( link ).click( function ( e ) { if ( e.metaKey ) { // blur current editable. user is waiting for the link to load Aloha.activeEditable.blur(); // hack to guarantee a browser history entry window.setTimeout( function () { location.href = e.target; }, 0 ); e.stopPropagation(); return false; } } ); }, /** * Initialize the buttons */ createButtons: function () { var that = this; this._formatLinkButton = Ui.adopt("formatLink", ToggleButton, { tooltip: i18n.t("button.addlink.tooltip"), icon: "aloha-icon aloha-icon-link", scope: 'Aloha.continuoustext', click: function() { that.formatLink(); } }); this._insertLinkButton = Ui.adopt("insertLink", Button, { tooltip: i18n.t("button.addlink.tooltip"), icon: "aloha-icon aloha-icon-link", scope: 'Aloha.continuoustext', click: function() { that.insertLink(false); } }); this.hrefField = AttributeField({ name: 'editLink', width: 320, valueField: 'url', cls: 'aloha-link-href-field', scope: 'Aloha.continuoustext', noTargetHighlight: true, targetHighlightClass: 'aloha-focus' }); this.hrefField.setTemplate('{name}