/* floating.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. * * @overview * Implements position and floating animation effect for UI surfaces. */ define([ 'jquery', 'aloha/core', 'ui/surface', 'ui/subguarded', 'PubSub', 'vendor/amplify.store', 'util/browser' ], function ( $, Aloha, Surface, subguarded, PubSub, amplifyStore, Browser ) { 'use strict'; /** * The distance that the floating surface should maintain from the editable * it is floating to. * * @type {string} * @const */ var DISTANCE = 10; /** * The duration of the floating animation in milliseconds. * * @type {number} * @const */ var DURATION = 500; /** * jQuery unit set containing a feference to the global window. * * @type {jQuery.} * @const */ var $WINDOW = $(window); /** * The "position" style value. * * IE 7 does not support "fixed" position styling. Since "fixed" position * results in smoother animation the use of "absolute" is made as a special * accomodation for IE 7. * * @type {string} * @const */ var POSITION_STYLE = Browser.ie7 ? 'absolute' : 'fixed'; /** * The position of the floating menu. * * Used to float dialoges (eg special character-picker) with the floating * menu. * * @type {object} */ var POSITION = { style: POSITION_STYLE, offset: { top: 0, left: 0 } }; /** * Animates a surface element to the given position. * * @param {jQuery.} $element jQuery unit set of the DOM * element to move. * @param {object} position The x and y position to which the element * should end up. * @param {number} duration The length of time (in milliseconds) that the * animation should run for. * @param {function} callback Function to be invoked when animation * completes. */ function floatTo($element, position, duration, callback) { if ('absolute' === POSITION_STYLE) { position.top += $WINDOW.scrollTop(); position.left += $WINDOW.scrollLeft(); } POSITION.offset = position; $element.stop().animate(position, duration, function () { callback(position); PubSub.pub('aloha.floating.changed', { position: $.extend({}, POSITION) }); }); } /** * Moves an element above the given coordinates. * * @param {jQuery.} $element jQuery unit set of the DOM * element to move. * @param {object} position The x and y position to which the element * should end up. * @param {number} duration The length of time (in milliseconds) that the * animation should run for. * @param {function} callback Function to be invoked when animation * completes. */ function floatAbove($element, position, duration, callback) { position.top -= $element.height() + DISTANCE; floatTo($element, position, duration, callback); } /** * Moves the element below the given coordinates. * * @param {jQuery.} $element jQuery unit set of the DOM * element to move. * @param {object} position The x and y position to which the element * should end up. * @param {number} duration The length of time (in milliseconds) that the * animation should run for. * @param {function} callback Function to be invoked when animation * completes. */ function floatBelow($element, position, duration, callback) { position.top += DISTANCE; floatTo($element, position, duration, callback); } /** * Persist the "top" and "left" positions of the FloatingMenu surface. */ function storePinPosition(offset) { amplifyStore.store('Aloha.FloatingMenu.pinned', 'true'); amplifyStore.store('Aloha.FloatingMenu.top', offset.top); amplifyStore.store('Aloha.FloatingMenu.left', offset.left); } /** * Clears persisted state of the FloatingMenu surface. */ function unstorePinPosition() { amplifyStore.store('Aloha.FloatingMenu.pinned', null); amplifyStore.store('Aloha.FloatingMenu.top', null); amplifyStore.store('Aloha.FloatingMenu.left', null); } /** * Retreive the persisted pinned position of the FloatingMenu surface. * * @return {object} */ function getPinState() { if (amplifyStore.store('Aloha.FloatingMenu.pinned') === 'true') { return { top: parseInt(amplifyStore.store('Aloha.FloatingMenu.top'), 10), left: parseInt(amplifyStore.store('Aloha.FloatingMenu.left'), 10), isPinned: true }; } return { top: null, left: null, isPinned: false }; } /** * Constrains the given position coordinates to be within the viewport. * * @param {object} position "Top" and "left" coordinates. * @param {object} Constrained "top" and "left" coordinates. */ function forcePositionIntoWindow(position) { var left = position.left; var top = position.top; if (top < 0) { top = 0; } else if (top > $WINDOW.height()) { top = $WINDOW.height() / 2; } if (left < 0) { left = 0; } else if (left > $WINDOW.width()) { left = $WINDOW.width() / 2; } return { top: top, left: left }; } /** * Floats a surface to the appropriate position around the given editable. * * @param {Surface} surface The surface to be positioned. * @param {Aloha.Editable} editable The editable around which the surface * should be positioned. * @param {number} duration The length of time (in milliseconds) for the * animation should run. * @param {function} callback Function to be invoked after the animation * is completed. */ function floatSurface(surface, editable, duration, callback) { if (typeof duration !== 'number') { duration = DURATION; } var topGutter = (parseInt($('body').css('marginTop'), 10) || 0) + (parseInt($('body').css('paddingTop'), 10) || 0); var $surface = surface.$element; var offset = editable.obj.offset(); var top = offset.top; var left = offset.left; var scrollTop = $WINDOW.scrollTop(); var availableSpace = top - scrollTop - topGutter; var horizontalOverflow = left + $surface.width() - $WINDOW.width(); if (horizontalOverflow > 0) { left = Math.max(0, left - horizontalOverflow); } if (availableSpace >= $surface.height()) { floatAbove($surface, { top: top - scrollTop, left: left }, duration, callback); } else if (availableSpace + $surface.height() > availableSpace + editable.obj.height()) { floatBelow($surface, { top: top + editable.obj.height(), left: left }, duration, callback); } else { floatBelow($surface, { top: topGutter, left: left }, duration, callback); } } /** * Pins a surface at the speficied position on the viewport. * * @param {Surface} surfaces The surfaces that are to be pinned. * @param {object} position The "top" and "left" position of where the * surface is to be pinned. * @param {boolean} isFloating Whether or not the surface type is in * "floating" mode or not. */ function togglePinSurface(surface, position, isFloating) { var $surface = surface.$element; if (isFloating) { unstorePinPosition(); $surface.find('.aloha-ui-pin').removeClass('aloha-ui-pin-down'); } else { storePinPosition(position); $surface.find('.aloha-ui-pin').addClass('aloha-ui-pin-down'); } $surface.css({ position: 'fixed', top: position.top }); } /** * Filters surface activation events. */ function onActivatedSurface(tuples, eventName, $event, range, nativeEvent) { var i; for (i = 0; i < tuples.length; i++) { if (tuples[i][0].isActive()) { tuples[i][1]($event, range, nativeEvent); } } } /** * Sets the surface's DOM element's "position" property to "fixed." * * IE7 will not properly set the position property to "fixed" if our * element is not rendered. We therefore have to do a rigmarole to * temorarily render the element in order to set the position correctly. * * @param {Surface} surface */ function setPositionStyleToFixed(surface) { if ($.browser.msie) { var $parent = surface.$element.parent(); surface.$element.appendTo('body'); surface.$element.css('position', POSITION_STYLE); if ($parent.length) { surface.$element.appendTo($parent); } else { surface.$element.detach(); } } else { surface.$element.css('position', POSITION_STYLE); } } /** * Binds floating facilities on a surface. * * @TODO: * Resizable toolbars are possible, and would be a nice feature: * surface.$element.resizable(); * * @param {Surface} surface A UI Surface instance. * @param {object} SurfaceTypeManager */ function makeFloating(surface, SurfaceTypeManager) { subguarded([ 'aloha-selection-changed', 'aloha.ui.container.selected' ], onActivatedSurface, surface, function () { surface._move(); }); var updateSurfacePosition = function () { var position = forcePositionIntoWindow({ top: SurfaceTypeManager.pinTop, left: SurfaceTypeManager.pinLeft }); SurfaceTypeManager.setFloatingPosition(position); surface.$element.css({ top: position.top, left: position.left }); }; $WINDOW.scroll(function () { // TODO: only do this for active surfaces. surface._move(0); }); $WINDOW.resize(function () { if (!SurfaceTypeManager.isFloatingMode) { updateSurfacePosition(); } }); surface.addPin(); setPositionStyleToFixed(surface); if (!SurfaceTypeManager.isFloatingMode) { updateSurfacePosition(); } surface.$element.css('z-index', 10100).draggable({ distance: 20, stop: function (event, ui) { SurfaceTypeManager.setFloatingPosition(ui.position); if (!SurfaceTypeManager.isFloatingMode) { storePinPosition(ui.position); } } }); } return { getPinState: getPinState, makeFloating: makeFloating, floatSurface: floatSurface, togglePinSurface: togglePinSurface, POSITION_STYLE: POSITION_STYLE }; });