/* block.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-2012 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.
*/
/**
* Module which contains the base class for Blocks, and a Default/Debug block.
*
* @name block.block
* @namespace block/block
*/
define([
'aloha',
'jquery',
'block/blockmanager',
'aloha/observable',
'ui/scopes',
'util/class',
'PubSub'
], function(
Aloha,
jQuery,
BlockManager,
Observable,
Scopes,
Class,
PubSub
){
'use strict';
var GENTICS = window.GENTICS;
/**
* An aloha block has the following special properties, being readable through the
* "attr" function:
* - aloha-block-type -- TYPE of the AlohaBlock as registered by the BlockManager
*
* @name block.block.AbstractBlock
* @class An abstract block that must be used as a base class for custom blocks
*/
var AbstractBlock = Class.extend(Observable,
/** @lends block.block.AbstractBlock */
{
/**
* Event which is triggered if the block attributes change.
*
* @name block.block.AbstractBlock#change
* @event
*/
/**
* Title of the block, used to display the name in the sidebar editor.
*
* @type String
* @api
*/
title: null,
/**
* Id of the underlying $element, used to identify the block.
* @type String
*/
id: null,
/**
* The wrapping element of the block.
* @type jQuery
* @api
*/
$element: null,
/**
* if TRUE, the rendering is currently taking place. Used to prevent recursion
* errors.
* @type Boolean
*/
_currentlyRendering: false,
/**
* set to TRUE once the block is fully initialized.
*
* @type Boolean
*/
_initialized: false,
/**
* Set to TRUE if the last click activated a *nested editable*.
* If FALSE; the block itself is selected.
* This is needed when a block is deleted in IE7/8.
*/
_isInsideNestedEditable: false,
/**************************
* SECTION: Initialization and Lifecycle
**************************/
/**
* Initialize the basic block. Do not call directly; instead use jQuery(...).alohaBlock() to
* create new blocks.
*
* This function shall only be called through the BlockManager. See BlockManager::_blockify().
*
* When a block is fully initialized, an "aloha.block.initialized"
* message is published.
*
* @param {jQuery} $element Element that declares the block
* @param {Object} attributes that shall be set to the block
* @constructor
*/
_constructor: function ($element, attributes) {
var that = this;
this.$element = $element;
if ($element.attr('id')) {
this.id = $element.attr('id');
} else {
this.id = GENTICS.Utils.guid();
$element.attr('id', this.id);
}
$element.contentEditable(false);
$element.addClass('aloha-block');
if (this.isDraggable()) {
// Remove default drag/drop behavior of the browser
$element.find('img').attr('draggable', 'false');
$element.find('a').attr('draggable', 'false');
}
// set the attributes
jQuery.each(attributes, function (k, v) {
that._setAttribute(k, v);
});
// While the event handler is defined here, it is connected to the DOM element inside "_connectThisBlockToDomElement"
this._onElementClickHandler = function (event) {
// We only activate ourselves if we are the innermost aloha-block.
// If we are not the innermost aloha-block, we get highlighted (but not activated) automatically
// by the innermost block.
if (jQuery(event.target).closest('.aloha-block').get(0) === that.$element.get(0)) {
that._fixScrollPositionBugsInIE();
that.activate(event.target, event);
}
};
// Register event handlers on the block
this._connectThisBlockToDomElement($element, function () {
PubSub.pub('aloha.block.initialized', {block: that});
});
// This is executed when a block is selected through caret handling
// TODO!
//Aloha.bind('aloha-block-selected', function(event,obj) {
// if (that.$element.get(0) === obj) {
// that.activate();
// }
//});
this._initialized = true;
},
/**
* Is set inside the constructor to the event handler function
* which should be executed when the element is clicked.
*
* NOTE: Purely internal, "this" is not available inside this method!
*/
_onElementClickHandler: null,
/**
* We need to tell Aloha that we handle the event already;
* else a selection of a nested editable will *not* select
* the block.
*
* This callback is bound to the mousedown, focus and dblclick events.
*
* NOTE: Purely internal, "this" is not available inside this method!
*/
_preventSelectionChangedEventHandler: function ($event) {
if (('dblclick' !== $event.type)
&& !jQuery($event.target).is('.aloha-editable')) {
Aloha.Selection.preventSelectionChanged();
}
},
/**
* This method connects this block object to the passed DOM element.
* In detail, this method does the following:
*
* - if this.$element is already set, remove all block event handlers
* - sets this.$element = jQuery(newElement)
* - initialize event listeners on this.$element
* - call init()
*
* The method is called in two contexts: First, when a block is constructed
* to initialize the event listeners etc. Second, it is ALSO called when
* a block inside a nested block with editable in between is detected
* as inconsistent.
*/
_connectThisBlockToDomElement: function(newElement, callback) {
var that = this;
var $newElement = jQuery(newElement);
this._disconnectFromDomElement();
this.$element = $newElement;
this.$element.bind('click', this._onElementClickHandler);
this.$element.bind('mousedown', this._preventSelectionChangedEventHandler);
this.$element.bind('focus', this._preventSelectionChangedEventHandler);
this.$element.bind('dblclick', this._preventSelectionChangedEventHandler);
this.init(this.$element, function() {
// WORKAROUND against loading order dependencies. If we have
// nested Blocks inside each other (with no editables in between)
// it could be that the *inner* block is initialized *before* the outer one.
//
// However, the inner block needs to know whether it shall render drag handles or not,
// and this depends on whether it is inside an editable or a block.
//
// In order to fix this case, we delay the the drag-handle-rendering (and all the other
// post-processing) to the next JavaScript Run Loop using a small timeout.
window.setTimeout(function() {
that._postProcessElementIfNeeded();
if (callback) {
callback();
}
}, 5);
});
},
/**
* Disconnect the block from the DOM element
*/
_disconnectFromDomElement: function() {
if (this.$element) {
this.$element.unbind('click', this._onElementClickHandler);
this.$element.unbind('mousedown', this._preventSelectionChangedEventHandler);
this.$element.unbind('focus', this._preventSelectionChangedEventHandler);
this.$element.unbind('dblclick', this._preventSelectionChangedEventHandler);
}
},
/**
* IE HACK: Our beloved Internet Explorer sometimes scrolls to the top
* of the page when activating an aloha block, and on numerous other occasions
* like when an block is moved via drag/drop.
*
* We can detect this and scroll right back; although this will flicker
* a little (but still a lot better than before)
*/
_fixScrollPositionBugsInIE: function() {
var scrollPositionBefore = jQuery(window).scrollTop();
window.setTimeout(function() {
if (jQuery(window).scrollTop() !== scrollPositionBefore) {
jQuery(window).scrollTop(scrollPositionBefore);
}
}, 10);
},
/**
* Template method to initialize the block. Can be used to set attributes
* on the block, depending on the block contents. You will most probably
* use $element and this.attr() inside this function.
*
* !!! This method can be called *multiple times*, as it is called each time
* when $element has been disconnected from the DOM (which can happen because of various reasons)
* and the block needs to re-initialize. So make sure this method can be called *MULTIPLE TIMES*
* and always returns predictable results. This method must be idempotent, same as update().
*
* Furthermore, always when this method is finished, you need to call postProcessFn() afterwards.
* This function adds drag handles and other controls if necessary.
*
* @param {jQuery} $element a shortcut to the block's DOM element (this.$element) for easy processing
* @param {Function} postProcessFn this function MUST be called at all times the $element has been updated; as it adds drag/drop/delete/... handles if necessary
* @api
*/
init: function($element, postProcessFn) {
postProcessFn();
},
/**
* Callback which is executed when somebody triggers destroy().
*
* This only allows destruction if the block is *inside* an aloha-editable and *not* inside an aloha-block.
*
* @return {Boolean} true of destruction should happen, false otherwise
*/
shouldDestroy: function() {
var $closest = this.$element.parent().closest('.aloha-block,.aloha-editable,.aloha-block-collection');
if ($closest.hasClass('aloha-block-collection') && this.$element[0].tagName.toLowerCase() === 'div') {
return true;
} else {
return $closest.hasClass('aloha-editable');
}
},
/**
* Destroy this block instance completely. Removes the element from the DOM,
* unregisters it, and triggers a block-delete event on the BlockManager.
*
* @param {Boolean} force TRUE if you want to force deletion, despite shouldDestroy() returning false.
* @api
*/
destroy: function(force) {
if (!this.shouldDestroy() && force !== true) return;
var that = this;
var newRange = new GENTICS.Utils.RangeObject();
newRange.startContainer = newRange.endContainer = this.$element.parent()[0];
newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent(this.$element[0]);
BlockManager.trigger('block-delete', this);
this.free();
var isInlineElement = this.$element[0].tagName.toLowerCase() === 'span';
this.$element.fadeOut('fast', function() {
that.$element.remove();
BlockManager.trigger('block-selection-change', []);
window.setTimeout(function() {
if (isInlineElement) {
newRange.select();
}
}, 5);
});
},
/**
* Remove this block, but leave the original DOM element
*/
unblock: function () {
// TODO set old value of contentEditable
// TODO set old values for draggable attributes
// deactivate
this.deactivate();
// remove handlers
this._disconnectFromDomElement();
// remove block class
this.$element.removeClass('aloha-block');
// remove block handles
this.$element.children('.aloha-block-handle').remove();
// unregister the block
this.free();
},
/**
* Free internal state associated with this block.
*
* Should be called when a block is not used any more to prevent
* memory leaks.
*
* Any invokations of instance methods after this method has
* been called will result in undefined behaviour.
*
* @api
*/
free: function () {
BlockManager._unregisterBlock(this);
this.unbindAll();
},
/**************************
* SECTION: Getters and Helpers
**************************/
/**
* Get the id of the block
* @returns {String}
*/
getId: function() {
return this.id;
},
/**
* Get a schema of attributes which shall be rendered / edited
* in the sidebar.
*
* @api
* @returns {Object}
*/
getSchema: function() {
return null;
},
/**
* Template Method which should return the block title. Needed for editing sidebar.
* By default, the block title is returned.
*
* @api
*/
getTitle: function() {
return this.title;
},
/**
* Returns true if the block is draggable because it is inside an aloha-editable, false otherwise.
*
* You cannot depend on this method's result during the *init* phase of the Aloha Block, as the
* outer block might not be initialized at that point yet. Thus, do not call this method inside init().
*
* @return Boolean
*/
isDraggable: function() {
if (this.$element[0].tagName.toLowerCase() === 'div' && this.$element.parents('.aloha-editable,.aloha-block,.aloha-block-collection').first().hasClass('aloha-block-collection')) {
// Here, we are inside an aloha-block-collection, and thus also need to be draggable.
return true;
}
return this.$element.parents('.aloha-editable,.aloha-block').first().hasClass('aloha-editable');
},
/**************************
* SECTION: Activation / Deactivation
**************************/
/**
* activates the block
* will select the block's contents, highlight it, update the floating menu and update the sidebar (if needed).
*
* When calling programmatically, do not set eventTarget or event arguments.
* @api
*/
activate: function(eventTarget, event) {
var highlightedBlocks = [];
// Deactivate currently highlighted blocks
jQuery.each(BlockManager._getHighlightedBlocks(), function() {
this.deactivate();
});
// Activate current block
if (this.$element.attr('data-block-skip-scope') !== 'true') {
Scopes.setScope('Aloha.Block.' + this.attr('aloha-block-type'));
}
this.$element.addClass('aloha-block-active');
this._highlight();
highlightedBlocks.push(this);
// Highlight parent blocks
this.$element.parents('.aloha-block').each(function() {
var block = BlockManager.getBlock(this);
if (block) {
block._highlight();
highlightedBlocks.push(block);
}
});
// Browsers do not remove the cursor, so we enforce it when an aditable is clicked.
// However, when the user clicked inside a nested editable, we will not remove the cursor (as the user wants to start typing then)
// small HACK: we also do not deactivate if we are inside an aloha-table-cell-editable.
if (jQuery(eventTarget).closest('.aloha-editable,.aloha-block,.aloha-table-cell-editable,.aloha-table-cell_active').first().hasClass('aloha-block')) {
this._isInsideNestedEditable = false;
Aloha.getSelection().removeAllRanges();
} else {
this._isInsideNestedEditable = true;
if (event) {
// We now update the selection, as you clicked *inside* an editable inside the block
Aloha.Selection.updateSelection(event);
}
}
// Trigger block activate & selection change events.
BlockManager.trigger('block-activate', highlightedBlocks);
BlockManager.trigger('block-selection-change', highlightedBlocks);
},
/**
* Deactive the block
*/
deactivate: function() {
var that = this;
var deactivatedBlocks = [this];
this._unhighlight();
this.$element.parents('.aloha-block').each(function() {
deactivatedBlocks.push(this);
that._unhighlight();
});
this.$element.removeClass('aloha-block-active');
// Trigger block deactivate & selection change events.
BlockManager.trigger('block-deactivate', deactivatedBlocks);
BlockManager.trigger('block-selection-change', []);
},
/**
* @returns {Boolean} True if this block is active
*/
isActive: function() {
return this.$element.hasClass('aloha-block-active');
},
/**
* Internal helper which sets a block as highlighted, because the block itself
* or a child block has been activated.
*/
_highlight: function() {
this.$element.addClass('aloha-block-highlighted');
BlockManager._setHighlighted(this);
},
/**
* Internal helper which sets a block as un-highlighted.
*/
_unhighlight: function() {
this.$element.removeClass('aloha-block-highlighted');
BlockManager._setUnhighlighted(this);
},
/**************************
* SECTION: Block Rendering
**************************/
/**
* Internal _update method, which needs to be called internally if a property
* changed. This is just a wrapper around update().
*/
_update: function() {
var that = this;
if (this._currentlyRendering) return;
if (!this._initialized) return;
this._currentlyRendering = true;
this.update(this.$element, function() {
that._postProcessElementIfNeeded();
});
this._currentlyRendering = false;
},
/**
* Template method to render contents of the block, must be implemented by specific block type.
* $element can be augumented by additional DOM elements like drag/drop handles. If you do
* any jQuery selection, you need to ignore all results which have a "aloha-block-handle" class
* set.
*
* Furthermore, always when you update $element, you need to call postProcessFn() afterwards.
* This function adds drag handles and other controls if necessary.
*
* This method should *only* be called from the internal _update method.
*
* @param {jQuery} $element a shortcut to the block's DOM element (this.$element) for easy processing
* @param {Function} postProcessFn this function MUST be called at all times the $element has been updated; as it adds drag/drop/delete/... handles if necessary
*
* @api
*/
update: function($element, postProcessFn) {
postProcessFn();
},
/**
* Post processor, being called to augument the Block Element's DOM by drag handles etc.
*
* This method must be idempotent. I.e. it must produce the same results
* when called once or twice.
*/
_postProcessElementIfNeeded: function() {
this.createEditablesIfNeeded();
this._checkThatNestedBlocksAreStillConsistent();
this._makeNestedBlockCollectionsSortable();
this.renderBlockHandlesIfNeeded();
if (this.isDraggable() && this.$element[0].tagName.toLowerCase() === 'span') {
this._setupDragDropForInlineElements();
this._disableUglyInternetExplorerDragHandles();
} else if (this.isDraggable() && this.$element[0].tagName.toLowerCase() === 'div') {
this._setupDragDropForBlockElements();
this._disableUglyInternetExplorerDragHandles();
}
this._hideDragHandlesIfDragDropDisabled();
this._attachDropzoneHighlightEvents();
},
/**
* Due to indeterminate initialization order of nested blocks,
* it can happen that blockifying a parent block deconnects $element inside
* child blocks.
*
* This is the case we detect here; and if it happens, we reconnect the
* block to its currently visible DOM element.
*/
_checkThatNestedBlocksAreStillConsistent: function() {
this.$element.find('.aloha-block').each(function() {
var block = BlockManager.getBlock(this);
if (block && block.$element[0] !== this) {
block._connectThisBlockToDomElement(this);
}
});
},
/**
* If a nested element is marked as "aloha-block-collection",
* we want to make it sortable, by calling the appropriate Block Manager
* function.
*/
_makeNestedBlockCollectionsSortable: function() {
var that = this;
this.$element.find('.aloha-block-collection').each(function() {
var $blockCollection = jQuery(this);
if ($blockCollection.closest('.aloha-block').get(0) === that.$element.get(0)) {
// We are only responsible for one-level-down Block Collections, not
// for nested ones.
BlockManager.createBlockLevelSortableForEditableOrBlockCollection($blockCollection);
}
})
},
/**
* Helper which disables the ugly IE drag handles. They are still shown, but at
* least they do not work anymore
*/
_disableUglyInternetExplorerDragHandles: function() {
if (jQuery.browser.msie) {
this.$element.get( 0 ).onresizestart = function ( e ) { return false; };
this.$element.get( 0 ).oncontrolselect = function ( e ) { return false; };
// We do NOT abort the "ondragstart" event as it is required for drag/drop.
this.$element.get( 0 ).onmovestart = function ( e ) { return false; };
// We do NOT abort the "onselectstart" event because this would disable selection in nested editables
}
},
/**
* Removes the draghandle class from block handle,
* if drag & drop is disabled for the editable
*/
_hideDragHandlesIfDragDropDisabled: function() {
if ( !this._dd_isDragdropEnabled() ){
this.$element.find('.aloha-block-draghandle').each(function () {
var $draghandle = jQuery(this);
if (!isDragdropEnabledForElement($draghandle)) {
$draghandle.removeClass('aloha-block-draghandle');
}
});
}
},
/**
* Attach mousedown/up events to block's draghandle
* to toggle dropzones when dragging starts and ends.
*/
_attachDropzoneHighlightEvents: function() {
var that = this;
this.$element.delegate( ".aloha-block-draghandle", "mousedown", function() {
var dropzones = that.$element.parents( ".aloha-editable" ).first().data( "block-dropzones" ) || [];
jQuery.each( dropzones, function(i, editable_selector) {
var editables = jQuery( editable_selector );
jQuery( editables ).each(function() {
if (jQuery( this ).data( "block-dragdrop" )) {
jQuery( this ).addClass( "aloha-block-dropzone" );
}
});
});
// Remove the dropzones as soon as the mouse is released,
// irrespective of where the drop took place.
jQuery( document ).one( "mouseup.aloha-block-dropzone", function(e) {
var dropzones = that.$element.parents( ".aloha-editable" ).first().data( "block-dropzones" ) || [];
jQuery.each( dropzones, function(i, editable_selector) {
jQuery( editable_selector ).removeClass( "aloha-block-dropzone" );
});
});
});
},
/**************************
* SECTION: Drag&Drop for INLINE elements
**************************/
_setupDragDropForInlineElements: function() {
var that = this;
// Here, we store the character DOM element which has been hovered upon recently.
// This is needed as somehow, the "drop" event on the character is not fired.
// Furthermore, we use it to know whether we need to "revert" the draggable to the original state or not.
var lastHoveredCharacter = null;
// Unless this flag is set to true, drag operation should be reverted.
// Firing of "drop" event will set this to true.
var blockDroppedProperly = false;
// HACK for IE7: Internet Explorer 7 has a very weird behavior in
// not always firing the "drop" callback of the inner droppable... However,
// the "over" and "out" callbacks are fired correctly.
// Because of this, we handle the "drop" inside the "stop" callback in IE7
// instead of the "drop" callback (where it is handled in all other browsers)
// This $currentDraggable is also needed as part of the IE 7 hack.
// $currentDraggable contains a reference to the current draggable, but
// only makes sense to read when lastHoveredCharacter !== NULL.
var $currentDraggable = null;
// We need to store the droppables created at the start of the drag,
// they should be destroyed when the drag stops.
var $createdDroppables = null;
// This dropFn is the callback which handles the actual moving of
// nodes. We created a separate function for it, as it is called inside the "stop" callback
// in IE7 and inside the "drop" callback in all other browsers.
var dropFn = function() {
if (lastHoveredCharacter) {
// the user recently hovered over a character
var $dropReferenceNode = jQuery(lastHoveredCharacter);
if ($dropReferenceNode.is('.aloha-block-dropInlineElementIntoEmptyBlock')) {
// the user wanted to drop INTO an empty block!
$dropReferenceNode.children().remove();
$dropReferenceNode.append($currentDraggable);
} else if ($dropReferenceNode.is('.aloha-block-droppable-right')) {
$dropReferenceNode.html($dropReferenceNode.html() + ' ');
// Move draggable after drop reference node
$dropReferenceNode.after($currentDraggable);
} else {
// Insert space in the beginning of the drop reference node
if ($dropReferenceNode.prev('[data-i]').length > 0) {
// If not the last element, insert space in front of next element (i.e. after the moved block)
$dropReferenceNode.prev('[data-i]').html($dropReferenceNode.prev('[data-i]').html() + ' ');
}
$dropReferenceNode.html(' ' + $dropReferenceNode.html());
// Move draggable before drop reference node
$dropReferenceNode.before($currentDraggable);
}
$currentDraggable.removeClass('ui-draggable').css({'left': 0, 'top': 0}); // Remove "draggable" options... somehow "Destroy" does not work
that._fixScrollPositionBugsInIE();
}
jQuery('.aloha-block-dropInlineElementIntoEmptyBlock').removeClass('aloha-block-dropInlineElementIntoEmptyBlock');
// clear the created droppables
$createdDroppables.droppable( "destroy" );
$createdDroppables = null;
blockDroppedProperly = true;
};
var editablesWhichNeedToBeCleaned = [];
this.$element.draggable({
handle: '.aloha-block-draghandle',
scope: 'aloha-block-inlinedragdrop',
disabled: !this._dd_isDragdropEnabled(),
revert: function() {
return (lastHoveredCharacter === null || !blockDroppedProperly);
},
revertDuration: 250,
stop: function() {
if (jQuery.browser.msie && 7 === parseInt(jQuery.browser.version, 10)) {
dropFn();
}
jQuery.each(editablesWhichNeedToBeCleaned, function() {
that._dd_traverseDomTreeAndRemoveSpans(this);
});
$currentDraggable = null;
editablesWhichNeedToBeCleaned = [];
},
start: function() {
blockDroppedProperly = false;
editablesWhichNeedToBeCleaned = [];
// In order to make Inline Blocks droppable into empty paragraphs, we insert a manually before the placeholder-br.
// -> for IE
jQuery('.aloha-editable').children('p:empty').html(' ');
// Make **ALL** editables on the page droppable, such that it is possible
// to drag/drop *across* editable boundaries
var droppableCfg = {
// make block elements droppable
tolerance: 'pointer',
addClasses: false, // performance optimization
scope: 'aloha-block-inlinedragdrop',
/**
* When hovering over a paragraph, we make convert its contents into spans, to make
* them droppable.
*/
over: function(event, ui) {
if (jQuery.inArray(this, editablesWhichNeedToBeCleaned) === -1) {
editablesWhichNeedToBeCleaned.push(this);
}
var hasOnlyProppingBr = (
1 === jQuery(this).contents().length &&
1 === jQuery(this).children('br').length
);
$currentDraggable = ui.draggable;
if (jQuery(this).is(':empty') ||
hasOnlyProppingBr ||
jQuery(this).html() === ' ') {
// The user is hovering over an empty
// container; simply highlight the container.
jQuery(this).addClass(
'aloha-block-dropInlineElementIntoEmptyBlock');
lastHoveredCharacter = this;
return;
}
that._dd_traverseDomTreeAndWrapCharactersWithSpans(this);
jQuery('span[data-i]', this).droppable({
tolerance: 'pointer',
addClasses: false,
scope: 'aloha-block-inlinedragdrop',
over: function() {
if (lastHoveredCharacter) {
// Just to be sure, we remove the css class of the last hovered character.
// This is needed such that spans are deselected which contain multiple
// lines.
jQuery(lastHoveredCharacter).removeClass('aloha-block-droppable');
}
lastHoveredCharacter = this;
jQuery(this).addClass('aloha-block-droppable');
},
out: function() {
jQuery(this).removeClass('aloha-block-droppable');
if (lastHoveredCharacter === this) {
lastHoveredCharacter = null;
}
}
});
// Now that we updated the droppables in the system, we need to recalculate
// the Drag Drop offsets.
jQuery.ui.ddmanager.prepareOffsets(ui.draggable.data('draggable'), event);
},
out: function() {
jQuery(this).removeClass('aloha-block-dropInlineElementIntoEmptyBlock');
},
/**
* When dropping over a paragraph, we use the "lastHoveredCharacter"
* as drop target.
*/
drop: function() {
if (!(jQuery.browser.msie && 7 === parseInt(jQuery.browser.version, 10))) {
dropFn();
}
}
};
$createdDroppables = jQuery( ".aloha-editable.aloha-block-dropzone" ).children( ":not(.aloha-block)" );
$createdDroppables.droppable( droppableCfg );
// Small HACK: Also make table cells droppable
jQuery('.aloha-table-cell-editable').droppable(droppableCfg);
}
});
},
/**
* Helper which traverses the DOM tree starting from el and wraps all non-empty texts with spans,
* such that they can act as drop target.
*
* @param {DomElement} el
*/
_dd_traverseDomTreeAndWrapCharactersWithSpans: function(el) {
var child;
for(var i=0, l=el.childNodes.length; i < l; i++) {
child = el.childNodes[i];
if (child.nodeType === 1) { // DOM Nodes
if (!~child.className.indexOf('aloha-block') && child.attributes['data-i'] === undefined) {
// We only recurse if child does NOT have the class "aloha-block", and is NOT data-i
this._dd_traverseDomTreeAndWrapCharactersWithSpans(child);
} else if (child.attributes['data-i']) {
// data-i set -> we have converted this hierarchy level already --> early return!
return;
}
} else if (child.nodeType === 3) { // Text Nodes
var numberOfSpansInserted = this._dd_insertSpans(child);
i += numberOfSpansInserted;
l += numberOfSpansInserted;
}
}
},
/**
* Helper which splits text on word boundaries, adding whitespaces to the following element.
* Examples:
* - "Hello world" -> ["Hello", " world"]
* - " Hello world" -> [" Hello", " world"]
* --> see the unit tests for the specification
*/
_dd_splitText: function(text) {
var textParts = text.split(/(?=\b)/);
var cleanedTextParts = [];
var isWhitespace = false;
for (var i=0,l=textParts.length; i
' + k + ' | ' + v + ' |
---|