/* sidebar.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. */ /** * @todo: - Make the sidebars resizable using drag handles. * - Make overlayPage setting settable from external config. */ define([ 'aloha/core', 'jquery', 'aloha/selection', 'PubSub' ], function ( Aloha, $, Selection, PubSub ) { 'use strict'; var uid = +(new Date()); // Extend jQuery easing animations. //debugger; if (!$.easing.easeOutExpo) { $.extend($.easing, { easeOutExpo: function (x, t, b, c, d) { return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b; }, easeOutElastic: function (x, t, b, c, d) { var m = Math, s = 1.70158, p = 0, a = c; if (!t) { return b; } if ((t /= d) == 1) { return b + c; } if (!p) { p = d * 0.3; } if (a < m.abs(c)) { a = c; s = p / 4; } else { s = p / (2 * m.PI) * m.asin(c / a); } return a * m.pow(2, -10 * t) * m.sin((t * d - s) * (2 * m.PI) / p) + c + b; } }); } var Panel = function Panel(opts) { this.id = null; this.folds = {}; this.button = null; this.title = $('
' + '' + 'Untitled' + '
'); this.content = $('
' + '
' + '
' + '
' + '
'); this.element = null; this.effectiveElement = null; this.expanded = false; this.isActive = true; this.init(opts); }; var Sidebar = function Sidebar(opts) { var sidebar = this; this.id = 'aloha-sidebar-' + (++uid); this.panels = {}; this.container = $('
' + '
' + '' + '
' + '
' + '' + '
' + '
'); this.width = 300; this.opened = false; this.isOpen = false; this.isCompletelyOpen = false; this.settings = { // We automatically set this to true when we are in IE, where // rotating elements using filters causes undesirable rendering // ugliness. Our solution is to fallback to swapping icon images. // We set this as a sidebar property so that it can overridden by // whoever thinks they are smarter than we are. rotateIcons: !$.browser.msie, overlayPage: true }; $(function () { if (!((typeof Aloha.settings.sidebar !== 'undefined') && Aloha.settings.sidebar.disabled)) { sidebar.init(opts); } }); }; /** * The last calculated view port height. * @type {number} */ var previousViewportHeight = null; var previousActivePanelIds = null; $.extend(Sidebar.prototype, { // We build as much of the sidebar as we can before appending it to DOM // to minimize reflow. init: function (opts) { var that = this; var panels; if (typeof opts === 'object') { panels = opts.panels; delete opts.panels; } $.extend(this, opts); if (typeof panels === 'object') { $.each(panels, function () { that.addPanel(this, true); }); } var bar = this.container; if (this.position === 'right') { bar.addClass('aloha-sidebar-right'); } bar.hide().appendTo($('body')).click(function () { that.barClicked.apply(that, arguments); }).find('.aloha-sidebar-panels').width(this.width); // IE7 needs us to explicitly set the container width, since it is // unable to determine it on its own. bar.width(this.width); this.width = bar.width(); this.updateHeight(); this.initToggler(); this.container.css(this.position === 'right' ? 'marginRight' : 'marginLeft', -this.width); if (this.opened) { this.open(0); } this.toggleHandleIcon(this.isOpen); this.subscribeToEvents(); $(window).resize(function () { that.updateHeight(); that.correctHeight(); }); this.correctHeight(); }, show: function () { this.container.css('display', 'block'); return this; }, hide: function () { this.container.css('display', 'none'); return this; }, /** * Determines the effective elements at the current selection. * Iterates through all panels and checks whether the panel should be * activated for any of the effective elements in the selection. * * @param {Aloha.RangeObject} range The current selection range. */ checkActivePanels: function (range) { var effective = []; if (typeof range !== 'undefined' && typeof range.markupEffectiveAtStart !== 'undefined') { var l = range.markupEffectiveAtStart.length; var i; for (i = 0; i < l; ++i) { effective.push($(range.markupEffectiveAtStart[i])); } } var that = this; $.each(this.panels, function () { that.showActivePanel(this, effective); }); this.correctHeight(); }, subscribeToEvents: function () { var that = this; PubSub.sub('aloha.selection.context-change', function (message) { if (that.isOpen) { that.checkActivePanels(message.range); } that.lastRange = message.range; }); Aloha.bind('aloha-editable-deactivated', function (event, params) { if (that.isOpen) { that.checkActivePanels(); } that.lastRange = null; }); this.container.mousedown(function (e) { e.originalEvent.stopSelectionUpdate = true; Aloha.eventHandled = true; }); this.container.mouseup(function (e) { e.originalEvent.stopSelectionUpdate = true; Aloha.eventHandled = false; }); }, /** * Dynamically set appropriate heights for panels. * The height for each panel is determined by the amount of space that * is available in the viewport and the number of panels that need to * share that space. */ correctHeight: function () { if (!this.isOpen) { return; } if (!this.isCompletelyOpen) { this.correctHeightWhenCompletelyOpen = true; return; } var viewportHeight = $(window).height(); var activePanelIds = []; var panels = []; var panelId; for (panelId in this.panels) { if (this.panels.hasOwnProperty(panelId)) { if (this.panels[panelId].isActive) { panels.push(this.panels[panelId]); activePanelIds.push(panelId); } } } if (0 === panels.length) { return; } activePanelIds = activePanelIds.sort().join(','); if (previousActivePanelIds === activePanelIds && previousViewportHeight === viewportHeight) { return; } previousViewportHeight = viewportHeight; previousActivePanelIds = activePanelIds; var height = this.container.find('.aloha-sidebar-inner').height(); var remainingHeight = height - ((panels[0].title.outerHeight() + 10) * panels.length); var panel; var targetHeight; var panelInner; var panelText; var undone; var toadd = 0; var math = Math; var j; while (panels.length > 0 && remainingHeight > 0) { remainingHeight += toadd; toadd = 0; undone = []; for (j = panels.length - 1; j >= 0; --j) { panel = panels[j]; panelInner = panel.content.find('.aloha-sidebar-panel-content-inner'); targetHeight = math.min( panelInner.height('auto').height(), math.floor(remainingHeight / (j + 1)) ); panelInner.height(targetHeight); remainingHeight -= targetHeight; panelText = panelInner.find('.aloha-sidebar-panel-content-inner-text'); if (panelText.height() > targetHeight) { undone.push(panel); toadd += targetHeight; panelInner.css({ 'overflow-x': 'hidden', 'overflow-y': 'scroll' }); } else { panelInner.css('overflow-y', 'hidden'); } if (panel.expanded) { panel.expand(); } } panels = undone; } }, /** * Checks whether this panel should be activated (ie: made visible) for * any of the elements specified in a given list of elements. * * We have to add a null object to the list of elements to allow us to * check whether the panel should be visible when we have no effective * elements in the current selection * * @param {object} panel The Panel object we will test * @param {Array.>} elements The effective * elements, any of * which may activate * the panel. */ showActivePanel: function (panel, elements) { elements.push(null); var li = panel.content.parent('li'); var activeOn = panel.activeOn; var effective = $(); var count = 0; var j = elements.length; var i; for (i = 0; i < j; ++i) { if (activeOn(elements[i])) { ++count; if (elements[i]) { $.merge(effective, elements[i]); } } } if (count) { panel.activate(effective); } else { panel.deactivate(); } }, /** * Sets up the functionality, event listeners, and animation of the * sidebar handle */ initToggler: function () { var that = this; var bar = this.container; var icon = bar.find('.aloha-sidebar-handle-icon'); var toggledClass = 'aloha-sidebar-toggled'; var bounceTimer; var isRight = (this.position === 'right'); if (this.opened) { this.rotateHandleArrow(isRight ? 0 : 180, 0); } // configure the position of the sidebar handle $(function () { if (typeof Aloha.settings.sidebar !== 'undefined' && Aloha.settings.sidebar.handle && Aloha.settings.sidebar.handle.top) { $(bar.find('.aloha-sidebar-handle'))[0].style.top = Aloha.settings.sidebar.handle.top; } }); bar.find('.aloha-sidebar-handle').click(function () { if (bounceTimer) { clearInterval(bounceTimer); } icon.stop().css('marginLeft', 4); if (that.isOpen) { $(this).removeClass(toggledClass); that.close(); that.isOpen = false; } else { $(this).addClass(toggledClass); that.open(); that.isOpen = true; } }).hover(function () { var flag = that.isOpen ? -1 : 1; if (bounceTimer) { clearInterval(bounceTimer); } icon.stop(); $(this).stop().animate( isRight ? { marginLeft: '-=' + (flag * 5) } : { marginRight: '-=' + (flag * 5) }, 200 ); bounceTimer = setInterval(function () { flag *= -1; icon.animate( isRight ? { left: '-=' + (flag * 4) } : { right: '-=' + (flag * 4) }, 300 ); }, 300); }, function () { if (bounceTimer) { clearInterval(bounceTimer); } icon.stop().css(isRight ? 'left' : 'right', 5); $(this).stop().animate( isRight ? { marginLeft: 0 } : { marginRight: 0 }, 600, 'easeOutElastic' ); }); }, /** * Rounds the top corners of the first visible panel, and the bottom * corners of the last visible panel elements in the panels ul list. * @deprecated * @fixme: css3 */ roundCorners: function () { var bar = this.container; var lis = bar.find('.aloha-sidebar-panels>li:not(.aloha-sidebar-deactivated)'); var topClass = 'aloha-sidebar-panel-top'; var bottomClass = 'aloha-sidebar-panel-bottom'; bar.find('.aloha-sidebar-panel-top, .aloha-sidebar-panel-bottom').removeClass(topClass).removeClass(bottomClass); lis.first().find('.aloha-sidebar-panel-title').addClass(topClass); lis.last().find('.aloha-sidebar-panel-content').addClass(bottomClass); }, /** * Updates the height of the inner div of the sidebar. This is done * whenever the viewport is resized. */ updateHeight: function () { var h = $(window).height(); this.container.height(h).find('.aloha-sidebar-inner').height(h); }, /** * Delegate all sidebar onclick events to the container. * Then use handleBarclick method until we bubble up to the first * significant element that we can interact with. */ barClicked: function (ev) { this.handleBarclick($(ev.target)); }, /** * We handle all click events on the sidebar from here--dispatching * calls to which ever methods that should be invoked for the each * interaction. */ handleBarclick: function (el) { if (el.hasClass('aloha-sidebar-panel-title')) { this.togglePanel(el); } else if (!el.hasClass('aloha-sidebar-panel-content') && !el.hasClass('aloha-sidebar-handle') && !el.hasClass('aloha-sidebar-bar')) { this.handleBarclick(el.parent()); } }, getPanelById: function (id) { return this.panels[id]; }, getPanelByElement: function (el) { var li = (el[0].tagName === 'LI') ? el : el.parent('li'); return this.getPanelById(li[0].id); }, togglePanel: function (el) { this.getPanelByElement(el).toggle(); }, /** * Animation to rotate the sidebar arrow * * @param {number} angle The angle two which the arrow should rotate * (0 or 180). * @param {number|String} duration (Optional) How long the animation * should play for. */ rotateHandleIcon: function (angle, duration) { var arr = this.container.find('.aloha-sidebar-handle-icon'); arr.animate({ angle: angle }, { duration: (typeof duration === 'number' || typeof duration === 'string') ? duration : 500, easing: 'easeOutExpo', step: function (val, fx) { arr.css({ '-o-transform': 'rotate(' + val + 'deg)', '-webkit-transform': 'rotate(' + val + 'deg)', '-moz-transform': 'rotate(' + val + 'deg)', '-ms-transform': 'rotate(' + val + 'deg)' // We cannot use Microsoft Internet Explorer filters // because Microsoft Internet Explore 8 does not support // Microsoft Internet Explorer filters correctly. It // breaks the layout // filter : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')' }); } }); }, /** * Sets the handle icon to the "i am opened, click me to close the * sidebar" state, or vice versa. The direction of the arrow depends * on whether the sidebar is on the left or right, and whether it is * in an opened state or not. * * @param {boolean} isOpen Whether or not the sidebar is in the opened * state. */ toggleHandleIcon: function (isOpen) { var isPointingLeft = (this.position === 'right') ^ isOpen; if (this.settings.rotateIcons) { this.rotateHandleIcon(isPointingLeft ? 180 : 0, 0); } else { var icon = this.container.find('.aloha-sidebar-handle-icon'); if (isPointingLeft) { icon.addClass('aloha-sidebar-handle-icon-left'); } else { icon.removeClass('aloha-sidebar-handle-icon-left'); } } }, /** * Slides the sidebar into view */ open: function (duration, callback) { if (this.isOpen) { return this; } var isRight = (this.position === 'right'); var anim = isRight ? { marginRight: 0 } : { marginLeft: 0 }; var sidebar = this; this.toggleHandleIcon(true); this.container.animate(anim, (typeof duration === 'number' || typeof duration === 'string') ? duration : 500, 'easeOutExpo'); if (!this.settings.overlayPage) { $('body').animate( isRight ? { marginRight: '+=' + this.width } : { marginLeft: '+=' + this.width }, 500, 'easeOutExpo', function () { sidebar.isCompletelyOpen = true; if (sidebar.correctHeightWhenCompletelyOpen) { sidebar.correctHeight(); } } ); } this.isOpen = true; this.correctHeight(); if (this.lastRange) { this.checkActivePanels(this.lastRange); } $('body').trigger('aloha-sidebar-opened', this); return this; }, /** * Slides that sidebar out of view. */ close: function (duration, callback) { if (!this.isOpen) { return this; } var isRight = (this.position === 'right'); var anim = isRight ? { marginRight: -this.width } : { marginLeft: -this.width }; this.toggleHandleIcon(false); this.container.animate(anim, (typeof duration === 'number' || typeof duration === 'string') ? duration : 500, 'easeOutExpo'); if (!this.settings.overlayPage) { $('body').animate( isRight ? { marginRight: '-=' + this.width } : { marginLeft: '-=' + this.width }, 500, 'easeOutExpo' ); } this.isOpen = false; this.isCompletelyOpen = false; return this; }, /** * Activates the given panel and passes to it the given element as the * the effective that we want it to think activated it. * * @param {object|String} panel Panel instance or the id of a panel * object. * @param {jQuery} element Element to pass to the panel as effective * element (the element that activated it). */ activatePanel: function (panel, element) { if (typeof panel === 'string') { panel = this.getPanelById(panel); } if (panel) { panel.activate(element); } this.roundCorners(); return this; }, /** * Invokes the expand method for the given panel so that it expands its * height to display its contents * * @param {object|String} panel Panel instance or the id of a panel * object. * @param {funtion} callback */ expandPanel: function (panel, callback) { if (typeof panel === 'string') { panel = this.getPanelById(panel); } if (panel) { panel.expand(callback); } return this; }, /** * Collapses the panel contents by invoking the given panel's collapse * method. * * @param {object|String} panel Panel instance or the id of a panel * object. * @param {funtion} callback */ collapsePanel: function (panel, callback) { if (typeof panel === 'string') { panel = this.getPanelById(panel); } if (panel) { panel.collapse(callback); } return this; }, /** * Adds a panel to this sidebar instance. * We try and build as much of the panel DOM as we can before inserting * it into the DOM in order to reduce reflow. * * @param {object} panel - either a panel instance or an associative * array containing settings for the construction * of a new panel. * @param {boolean} deferRounding - (Optional) If true, the rounding-off * of the top most and bottom most panels * will not be automatically done. Set * this to true when adding a lot of panels * at once. * @return {object} The newly created panel. */ addPanel: function (panel, deferRounding) { if (!(panel instanceof Panel)) { if (!panel.width) { panel.width = this.width; } panel.sidebar = this; panel = new Panel(panel); } this.panels[panel.id] = panel; this.container.find('.aloha-sidebar-panels').append(panel.element); this.checkActivePanels(Selection.getRangeObject()); return panel; } }); // ------------------------------------------------------------------------ // Panel prototype // ------------------------------------------------------------------------ $.extend(Panel.prototype, { init: function (opts) { this.setTitle(opts.title).setContent(opts.content); delete opts.title; delete opts.content; $.extend(this, opts); if (!this.id) { this.id = 'aloha-sidebar-' + (++uid); } var li = this.element = $('
  • ').append(this.title, this.content); if (this.expanded) { this.content.height('auto'); } this.toggleTitleIcon(this.expanded); this.coerceActiveOn(); // Disable text selection on title element. this.title.attr('unselectable', 'on').css('-moz-user-select', 'none').each(function () { this.onselectstart = function () { return false; }; }); if (typeof this.onInit === 'function') { this.onInit.apply(this); } }, /** * @param {boolean} isExpanded Whether or not the panel is in an * expanded state. */ toggleTitleIcon: function (isExpanded) { if (this.sidebar.settings.rotateIcons) { this.rotateTitleIcon(isExpanded ? 90 : 0); } else { var icon = this.title.find('.aloha-sidebar-panel-title-arrow'); if (isExpanded) { icon.addClass('aloha-sidebar-panel-title-arrow-down'); } else { icon.removeClass('aloha-sidebar-panel-title-arrow-down'); } } }, /** * Normalizes the activeOn property into a predicate function. */ coerceActiveOn: function () { if (typeof this.activeOn !== 'function') { var activeOn = this.activeOn; this.activeOn = (function () { var typeofActiveOn = typeof activeOn, fn; if (typeofActiveOn === 'boolean') { fn = function () { return activeOn; }; } else if (typeofActiveOn === 'undefined') { fn = function () { return true; }; } else if (typeofActiveOn === 'string') { fn = function (el) { return el ? el.is(activeOn) : false; }; } else { fn = function () { return false; }; } return fn; }()); } }, /** * Activates (displays) this panel. */ activate: function (effective) { this.isActive = true; this.content.parent('li').show().removeClass('aloha-sidebar-deactivated'); this.effectiveElement = effective; if (typeof this.onActivate === 'function') { this.onActivate.call(this, effective); } }, /** * Hides this panel. */ deactivate: function () { if (!this.isActive) { return; } this.isActive = false; this.content.parent('li').hide().addClass('aloha-sidebar-deactivated'); this.effectiveElement = null; }, toggle: function () { if (this.expanded) { this.collapse(); } else { this.expand(); } }, /** * Displays the panel's contents. */ expand: function (callback) { var that = this; var el = this.content; var old_h = el.height(); var new_h = el.height('auto').height(); el.height(old_h).stop().animate( {height: new_h}, 500, 'easeOutExpo', function () { if (typeof callback === 'function') { callback.call(that); } } ); this.element.removeClass('collapsed'); this.toggleTitleIcon(true); this.expanded = true; return this; }, /** * Hides the panel's contents--leaving only it's header. */ collapse: function (duration, callback) { var that = this; this.element.addClass('collapsed'); this.content.stop().animate( { height: 5 }, 250, 'easeOutExpo', function () { if (typeof callback === 'function') { callback.call(that); } } ); this.toggleTitleIcon(false); this.expanded = false; return this; }, /** * May also be called by the Sidebar to update title of panel * * @param {string} html Markup string, DOM object, or jQuery object. */ setTitle: function (html) { this.title.find('.aloha-sidebar-panel-title-text').html(html); return this; }, /** * May also be called by the Sidebar to update content of panel * * @param {string|jQuery.|HTMLElement} html Markup string, * DOM object, or * jQuery object. */ setContent: function (html) { // We do this so that empty panels don't appear collapsed if (!html || html === '') { html = ' '; } this.content.find('.aloha-sidebar-panel-content-inner-text').html(html); return this; }, rotateTitleIcon: function (angle, duration) { var arr = this.title.find('.aloha-sidebar-panel-title-arrow'); arr.animate({ angle: angle }, { duration: (typeof duration === 'number') ? duration : 500, easing: 'easeOutExpo', step: function (val, fx) { arr.css({ '-o-transform': 'rotate(' + val + 'deg)', '-webkit-transform': 'rotate(' + val + 'deg)', '-moz-transform': 'rotate(' + val + 'deg)', '-ms-transform': 'rotate(' + val + 'deg)' // filter : 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + (angle / 90) + ')' }); } }); }, /** * Walks up the ancestors chain for the given effective element, and * renders subpanels using the specified renderer function. * * @param {jQuery.} effective The effective element, whose * lineage we want to render. * @param {function} renderer (Optional) function that will render each * element in the parental * lineage of the effective * element. */ renderEffectiveParents: function (effective, renderer) { var el = effective.first(); var content = []; var path = []; var activeOn = this.activeOn; var l; var pathRev; while (el.length > 0 && !el.is('.aloha-editable')) { if (activeOn(el)) { path.push('' + el[0].tagName.toLowerCase() + ''); l = path.length; pathRev = []; while (l--) { pathRev.push(path[l]); } content.push( '
    ' + '
    ' + pathRev.join('') + '
    ' + '
    ' + ((typeof renderer === 'function') ? renderer(el) : '----') + '
    ' + '
    ' ); } el = el.parent(); } this.setContent(content.join('')); $('.aloha-sidebar-panel-parent-path').click(function () { var $content = $(this).parent().find('.aloha-sidebar-panel-parent-content'); if ($content.hasClass('aloha-sidebar-opened')) { $content.hide().removeClass('aloha-sidebar-opened'); } else { $content.show().addClass('aloha-sidebar-opened'); } }); this.content.height('auto').find('.aloha-sidebar-panel-content-inner').height('auto'); } }); var left = new Sidebar({ position: 'left', width: 250 // TODO define in config }); var right = new Sidebar({ position: 'right', width: 250 // TODO define in config }); Aloha.Sidebar = { left: left, right: right }; return Aloha.Sidebar; });