/** * js/edit.js * Main hotglue frontend code * * Copyright Gottfried Haider, Danja Vasiliev 2010. * This source code is licensed under the GNU General Public License. * See the file COPYING for more details. */ $.glue.canvas = function() { return { update: function(elem) { if (elem === undefined) { elem = $('.object'); } var max_x = 0; var max_y = 0; $(elem).each(function() { var p = $(this).position(); if (max_x < p.left+$(this).outerWidth()) { max_x = p.left+$(this).outerWidth(); } if (max_y < p.top+$(this).outerHeight()) { max_y = p.top+$(this).outerHeight(); } }); // make body at least match the window width and height but don't // send these values to the backend in any case if (max_x < $(window).width()) { max_x = $(window).width(); } if (max_y < $(window).height()) { max_y = $(window).height(); } // resize body $('body').css('width', max_x+'px'); $('body').css('height', max_y+'px'); // update grid $.glue.grid.update(); } }; }(); $.glue.colorpicker = function() { var change_func = false; var color = false; var finish_func = false; var shown = false; // setup element var elem = $('
'); $(elem).children('#glue-colorpicker-wheel').farbtastic(function(col) { if (col !== color) { // update tooltip $(elem).children('#glue-colorpicker-wheel').find('.marker').attr('title', col); $(elem).children('#glue-colorpicker-transparent').removeClass('glue-colorpicker-transparent-set'); $(elem).children('#glue-colorpicker-transparent').addClass('glue-colorpicker-transparent-notset'); if (typeof change_func == 'function') { change_func(col); } color = col; } }); $(elem).children('#glue-colorpicker-transparent').bind('click', function(e) { $(this).addClass('glue-colorpicker-transparent-set'); $(this).removeClass('glue-colorpicker-transparent-notset'); if (typeof change_func == 'function') { change_func('transparent'); } color = 'transparent'; }); var close_colorpicker = function(e) { // close colorpicker when clicking outside of it or its children // note: this handler is also being called right after colorpicker // creation if (!$(e.target).hasClass('glue-ui') && $(e.target).parents('.glue-ui').length == 0) { // this also unregisters the event $.glue.colorpicker.hide(); // prevent the menu from firing e.stopImmediatePropagation(); } }; return { hide: function(cancel) { if (shown) { if (cancel === undefined || cancel == false) { if (typeof finish_func == 'function') { finish_func(color); } } $(elem).detach(); shown = false; } // unregister event $('body').unbind('click', close_colorpicker); }, is_shown: function() { return shown; }, set_color: function(col) { $.color.setColor(col); var rgba = $.color.getRGB(); var hex = $.color.getHex(); if ($(elem).children('#glue-colorpicker-transparent').css('display') == 'block') { // showing transparency button if (rgba.a == 0) { $(elem).children('#glue-colorpicker-transparent').addClass('glue-colorpicker-transparent-set'); $(elem).children('#glue-colorpicker-transparent').removeClass('glue-colorpicker-transparent-notset'); col = 'transparent'; } else { $(elem).children('#glue-colorpicker-transparent').removeClass('glue-colorpicker-transparent-set'); $(elem).children('#glue-colorpicker-transparent').addClass('glue-colorpicker-transparent-notset'); } } else { // not showing transparency button // a special case for color 'transparent' if (rgba.r == 0 && rgba.g == 0 && rgba.b == 0 && rgba.a == 0) { // set color to white hex = '#ffffff'; } } // set color wheel $.farbtastic($(elem).children('#glue-colorpicker-wheel')).setColor(hex); $(elem).children('#glue-colorpicker-wheel').find('.marker').attr('title', hex); color = col; }, show: function(def, transp, change, finish) { if (shown) { $.glue.colorpicker.hide(); } color = false; // set functions first, as $.farbtastic().setColor() immediately // triggers a change event change_func = change; finish_func = finish; if (transp) { $(elem).children('#glue-colorpicker-transparent').css('display', 'block'); } else { $(elem).children('#glue-colorpicker-transparent').css('display', 'none'); } if (typeof def != 'string' || def.length == 0) { // set a sane default $.farbtastic($(elem).children('#glue-colorpicker-wheel')).setColor('#ff0000'); $(elem).children('#glue-colorpicker-wheel').find('.marker').removeAttr('title'); } else { $.glue.colorpicker.set_color(def); } // add to dom $('body').append(elem); shown = true; // register event $('body').bind('click', close_colorpicker); } }; }(); $.glue.contextmenu = function() { var default_prio = 10; var left = []; var m = {}; var owner = false; var prev_owner = false; var top = []; var veto = {}; $('.object').live('glue-deselect', function(e) { // hide menu when deselecting $.glue.contextmenu.hide(); }); $('.object').live('glue-movestart', function(e) { // hide menu when moving the selected object if (this == owner) { prev_owner = owner; $.glue.contextmenu.hide(); } }); $('.object').live('glue-movestop', function(e) { // show menu again when we hid the menu because of movement if (this == prev_owner) { $.glue.contextmenu.show(prev_owner); prev_owner = false; } }); $('.object').live('glue-select', function(e) { // show menu when one object is selected if ($('.glue-selected').length == 1) { $.glue.contextmenu.show(this); } else { $.glue.contextmenu.hide(); } }); return { hide: function() { if (owner) { while (left.length) { var item = left.shift(); $(item.elem).trigger('glue-menu-deactivate'); $(item.elem).detach(); } while (top.length) { var item = top.shift(); $(item.elem).trigger('glue-menu-deactivate'); $(item.elem).detach(); } owner = false; } }, is_shown: function() { if (owner) { return true; } else { return false; } }, register: function(cls, name, elem, prio) { if (!m[cls]) { m[cls] = []; } if (prio === undefined) { prio = default_prio; } m[cls].push({ 'name': name, 'elem': elem, 'prio': prio }); }, reuse: function(cls, name, as, prio) { if (prio === undefined) { prio = default_prio; } for (var cur_m in m) { for (var i=0; i'); // set crucial css properties $(elem).addClass('glue-grid-y'); $(elem).addClass('glue-grid'); $(elem).addClass('glue-ui'); // use complementary color $(elem).css('background-color', $.xcolor.complementary(bg_color)); $(elem).css('height', grid_height+'px'); $(elem).css('left', x+'px'); $(elem).css('position', 'absolute'); $(elem).css('top', '0px'); $(elem).css('width', '1px'); $(elem).css('z-index', '200'); // add to dom and list $('body').append(elem); lines.push(elem); } for (var y=grid_y; y <= grid_height; y+=grid_y) { var elem = $('
'); $(elem).addClass('glue-grid-x'); $(elem).addClass('glue-grid'); $(elem).addClass('glue-ui'); // use complementary color $(elem).css('background-color', $.xcolor.complementary(bg_color)); $(elem).css('height', '1px'); $(elem).css('left', '0px'); $(elem).css('position', 'absolute'); $(elem).css('top', y+'px'); $(elem).css('width', grid_width+'px'); $(elem).css('z-index', '200'); $('body').append(elem); lines.push(elem); } // and guides for (var i in guides_x) { var elem = $('
'); $(elem).addClass('glue-guide-x'); $(elem).addClass('glue-guide'); $(elem).addClass('glue-ui'); // use a different color than background and grid lines $(elem).css('background-color', $.xcolor.average($.xcolor.complementary(bg_color), bg_color)); $(elem).css('height', grid_height+'px'); $(elem).css('left', guides_x[i]+'px'); $(elem).css('position', 'absolute'); $(elem).css('top', '0px'); $(elem).css('width', '1px'); $(elem).css('z-index', '200'); $('body').append(elem); guides.push(elem); } for (var i in guides_y) { var elem = $('
'); $(elem).addClass('glue-guide-y'); $(elem).addClass('glue-guide'); $(elem).addClass('glue-ui'); // use a different color than background and grid lines $(elem).css('background-color', $.xcolor.average($.xcolor.complementary(bg_color), bg_color)); $(elem).css('height', '1px'); $(elem).css('left', '0px'); $(elem).css('position', 'absolute'); $(elem).css('top', guides_y[i]+'px'); $(elem).css('width', grid_width+'px'); $(elem).css('z-index', '200'); $('body').append(elem); guides.push(elem); } } } else { remove(); } // bit 1 changes drag behavior // this is not working as expected (the object snaps every x/y pixels // form the current position, not from 0/0) // TODO (later): implement this properly if ((grid_mode & 2)) { $('.object').draggable('option', 'grid', [grid_x, grid_y]); } else { $('.object').draggable('option', 'grid', false); } // bit 2 changes resize behavior // this is not working as expected (the object snaps every x/y pixels // form the current position, not from 0/0) // TODO (later): implement this properly if ((grid_mode & 4)) { $('.resizable').resizable('option', 'grid', [grid_x, grid_y]); } else { $('.resizable').resizable('option', 'grid', false); } }; var remove = function() { // remove lines while (lines.length) { var line = lines.shift(); $(line).remove(); } // and guides while (guides.length) { var guide = guides.shift(); $(guide).remove(); } grid_height = false; grid_width = false; }; return { add_guide_x: function(y) { guides_x.push(y); }, add_guide_y: function(x) { guides_y.push(x); }, mode: function(val) { if (val === undefined) { return grid_mode; } else { grid_mode = val; // call update() to redraw } }, update: function(force) { if (force !== undefined && force) { grid_height = false; grid_width = false; } draw(); }, x: function(val) { if (val === undefined) { return grid_x; } else { grid_x = val; // call update() to redraw } }, y: function(val) { if (val === undefined) { return grid_y; } else { grid_y = val; // call update() to redraw } } }; }(); $.glue.menu = function() { var default_prio = 10; var cur = false; var m = {}; var prev_menu = ''; var spawn_coords = false; var close_menu = function(e) { // close menu when clicking outside of an ui element if (!$(e.target).hasClass('glue-ui') && $(e.target).parents('.glue-ui').length == 0) { // this also unregisters the event // when we close a menu like this we want to keep the name of the // previous menu, hence false $.glue.menu.hide(false); } }; $('.object').live('glue-select', function(e) { // hide any menu when an object gets selected if (cur) { $.glue.menu.hide(); } }); return { // hide any currently shown menus hide: function(reset_prev_menu) { // reset the previous menu, so we can launch the same menu immediately // for almost all callers (except close_menu above) if (reset_prev_menu === undefined || reset_prev_menu) { prev_menu = ''; } if (cur) { for (var i=0; i < cur.length; i++) { $(cur[i].elem).trigger('glue-menu-deactivate'); $(cur[i].elem).detach(); } cur = false; } $('body').unbind('click', close_menu); }, // return whether or not a menu is shown // menu .. menu name (if undefined, any menu) is_shown: function(menu) { if (menu === undefined) { if (cur) { return true; } else { return false; } } else { if (m[menu] && m[menu] == cur) { return true; } else { return false; } } }, prev_menu: function() { var ret = prev_menu; prev_menu = ''; return ret; }, // register a menu item // menu .. menu name // elem .. element to add // prio .. priority (ascending) - optional register: function(menu, elem, prio) { if (!m[menu]) { m[menu] = []; } if (prio === undefined) { prio = default_prio; } // add sorted by prio ascending var added = false; for (var i=0; i < m[menu].length; i++) { if (prio < m[menu][i].prio) { m[menu].splice(i, 0, { 'elem': elem, 'prio': prio }); added = true; break; } } if (!added) { m[menu].push({ 'elem': elem, 'prio': prio }); } }, // show a menu // this also hides any currently shown menus // menu .. menu name // x, y .. window coordinates to launch the menu show: function(menu, x, y) { if (!m[menu]) { return false; } // hide any active menu if (cur) { $.glue.menu.hide(); } // default x & y coordinates if (x === undefined) { x = $(window).width()/2; } if (y === undefined) { y = $(window).height()/2; } var max_w = 0; var max_h = 0; cur = m[menu]; // add items to dom num_shown = 0; for (var i=0; i < cur.length; i++) { var elem = cur[i].elem; // set crucial css properties $(elem).addClass('glue-menu-'+menu); $(elem).addClass('glue-menu'); $(elem).addClass('glue-ui'); $(elem).css('left', x+'px'); $(elem).css('position', 'fixed'); $(elem).css('top', y+'px'); $(elem).css('visibility', 'hidden'); $(elem).css('z-index', '201'); // add to dom $('body').append(elem); // trigger event $(elem).trigger('glue-menu-activate'); // check if we still want to show the icon ;) if ($(elem).css('display') == 'none') { continue; } else { num_shown++; } // calculate max width & height // make sure you specify the width & height attribute for images etc if (max_w < $(elem).outerWidth(true)) { max_w = $(elem).outerWidth(true); } if (max_h < $(elem).outerHeight(true)) { max_h = $(elem).outerHeight(true); } } // position items var num_rows = 1; while (num_rows*num_rows < num_shown) { num_rows++; } var num_cols = num_rows; if (num_shown <= num_rows*(num_rows-1)) { num_cols--; } var cur_row = 0; var cur_col = 0; for (var i=0; i < cur.length; i++) { var elem = cur[i].elem; // check if the icon is shown if ($(elem).css('display') == 'none') { continue; } if (cur_col == num_cols) { cur_row++; cur_col = 0; } // make visible $(elem).css('opacity', '0.0'); $(elem).css('visibility', ''); $(elem).animate({ left: (x-(num_rows*max_w)/2+cur_col*max_w)+'px', opacity: 1.0, top: (y-(num_rows*max_h)/2+cur_row*max_h)+'px' }, 200); cur_col++; } // register close menu event and set prev_menu $('body').bind('click', close_menu); prev_menu = menu; // convert x, y to page and save them spawn_coords = {x: $(document).scrollLeft()+x, y: $(document).scrollTop()+y}; return true; }, spawn_coords: function() { return spawn_coords; } }; }(); $.glue.object = function() { var alter_pre_save = {}; var resize_prev_grid = false; var reg_objs = {}; $('.resizable').live('glue-pre-clone', function(e) { // remove the jqueryui resizable-related stuff from the object $(this).removeClass('ui-resizable'); $(this).children('.ui-resizable-handle').remove(); }); $('.object').live('resize', function(e) { // ignore grid when ctrl is pressed if (e.ctrlKey) { if ($(this).resizable('option', 'grid') !== false) { // save previous setting resize_prev_grid = $(this).resizable('option', 'grid'); // disable grid $(this).resizable('option', 'grid', false); } } else { // reset previous setting if (resize_prev_grid) { $(this).resizable('option', 'grid', resize_prev_grid); resize_prev_grid = false; } } $.glue.object.resizable_update_tooltip(this); $(this).trigger('glue-resize'); }); $('.object').live('resizestart', function(e) { $(this).trigger('glue-resizestart'); }); $('.object').live('resizestop', function(e) { // reset previous grid setting if (resize_prev_grid) { $(this).resizable('option', 'grid', resize_prev_grid); resize_prev_grid = false; } $.glue.object.save(this); $(this).trigger('glue-resizestop'); $.glue.canvas.update(this); }); $(document).ready(function() { $.glue.object.register_alter_pre_save('resizable', function(obj, orig) { // remove the jqueryui resizable-related stuff from the object $(obj).removeClass('ui-resizable'); $(obj).children('.ui-resizable-handle').remove(); }); $.glue.object.register_alter_pre_save('object', function(obj, orig) { // remove the jqueryui draggable-related stuff from the object $(obj).removeClass('ui-draggable-dragging'); }); $.glue.object.register_alter_pre_save('glue-selected', function(obj, orig) { var border = $(orig).outerHeight()-$(orig).innerHeight(); var p = $(orig).position(); // remove class $(obj).removeClass('glue-selected'); // and remove border offset $(obj).css('left', (p.left+border/2)+'px'); $(obj).css('top', (p.top+border/2)+'px'); //$(obj).css('width', ($(orig).width()+border)+'px'); //$(obj).css('height', ($(orig).height()+border)+'px'); }); }); return { // obj .. element register: function(obj) { // prevent double registration if (reg_objs[$(obj).attr('id')]) { return false; } else { reg_objs[$(obj).attr('id')] = true; } // make sure everything has a z-index if (isNaN(parseInt($(obj).css('z-index')))) { $(obj).css('z-index', $.glue.stack.default_z()); } // obj must have width & height for draggable to work $(obj).draggable({ addClasses: false, distance: 10 }); // obj must not be an img element (otherwise resizable creates a // wrapper which fucks things up) if ($(obj).hasClass('resizable')) { $(obj).resizable(); $.glue.object.resizable_update_tooltip(obj); } $(obj).trigger('glue-register'); $.glue.canvas.update(obj); }, register_alter_pre_save: function(cls, func) { alter_pre_save[cls] = func; }, resizable_update_tooltip: function(obj) { var p = $(obj).position(); // don't include any border in the calculation $(obj).children('.ui-resizable-handle').attr('title', $(obj).innerWidth()+'x'+$(obj).innerHeight()+' at '+p.left+'x'+p.top); }, save: function(obj) { var elem = $(obj).clone(); var elem_cls = $(elem).attr('class').replace(/\s+/, ' ').split(' '); for (var i=0; i < elem_cls.length; i++) { if (typeof alter_pre_save[elem_cls[i]] == 'function') { alter_pre_save[elem_cls[i]](elem, obj); } } // trim element content // necessary, otherwise we'd be sending \n\t back again $(elem).html($.trim($(elem).html())); // convert to string var html = $('
').html(elem).html(); // DEBUG //console.log(html); $.glue.backend({ method: 'glue.save_state', 'html': html }); }, // obj .. element unregister: function(obj) { $(obj).trigger('glue-unregister'); // can't update canvas here as object to be deleted is still in the // dom } }; }(); $.glue.sel = function() { var drag_prev_grid = false; var drag_prev_x = false; var drag_prev_y = false; var drag_start_x = false; var drag_start_y = false; var drag_mouse_start_x = false; var drag_mouse_start_x = false; var key_moving = false; // this could probably also be body $('html').bind('click', function(e) { if (e.target == $('body').get(0)) { if ($('.glue-selected').length) { // deselect when clicking on background $.glue.sel.none(); // prevent the menu from firing e.stopImmediatePropagation(); } } }); $('html').bind('keydown', function(e) { if (e.which == 9) { // cycle through all objects with tab key if ($('.glue-selected').length < 2) { var next = false; if ($('.glue-selected').next('.object').length) { next = $('.glue-selected').next('.object'); } else { next = $('.object').first(); } if (next) { $.glue.sel.none(); $.glue.sel.select(next); // scroll to the selected objects if not currently visible var window_min_x = $(document).scrollLeft(); var window_max_x = window_min_x+$(window).width(); var window_min_y = $(document).scrollTop(); var window_max_y = window_min_y+$(window).height(); var h = $(next).outerHeight(); var p = $(next).position(); var w = $(next).outerWidth(); // fit the entire object on the screen // TODO (later): scroll a bit more up/left for the any // context menu to fit in there too if (p.left < window_min_x) { $(document).scrollLeft(p.left); } else if (window_max_x < p.left+w) { $(document).scrollLeft(window_min_x+p.left+w-window_max_x); } if (p.top < window_min_y) { $(document).scrollTop(p.top); } else if (window_max_y < p.top+h) { $(document).scrollTop(window_min_y+p.top+h-window_max_y); } } } return false; } else if (33 == e.which && e.shiftKey && $('.glue-selected').length) { // shift+pageup: move objects to top of stack // we can't use ctrl+page{up,down} as this cycles through tabs // only prevent scrolling here return false; } else if (34 == e.which && e.shiftKey && $('.glue-selected').length) { // shift+pagedown: move objects to bottom of stack return false; } else if (37 <= e.which && e.which <= 40 && $('.glue-selected').length) { // move selected elements with arrow keys var add_x = 0; var add_y = 0; if (e.which == 38) { add_y = -1; } else if (e.which == 39) { add_x = 1; } else if (e.which == 40) { add_y = 1; } else if (e.which == 37) { add_x = -1; } // shift multiplier if (e.shiftKey) { // this depends on the grid size add_x *= $.glue.grid.x(); add_y *= $.glue.grid.y(); } $('.glue-selected').not('.locked').each(function() { var p = $(this).position(); // prevent elements from going completely offscreen if (1 < p.left+add_x+$(this).outerWidth()) { $(this).css('left', (p.left+add_x)+'px'); } if (1 < p.top+add_y+$(this).outerHeight()) { $(this).css('top', (p.top+add_y)+'px'); } }); // scroll window if neccessary // TODO (later): implement for moving multiple objects if ($('.glue-selected').length == 1) { var window_min_x = $(document).scrollLeft(); var window_max_x = window_min_x+$(window).width(); var window_min_y = $(document).scrollTop(); var window_max_y = window_min_y+$(window).height(); var elem = $('.glue-selected'); var p = $(elem).position(); var w = $(elem).outerWidth(); var h = $(elem).outerHeight(); if (p.left < window_min_x) { $(document).scrollLeft(p.left); } else if (window_max_x < p.left+w) { $(document).scrollLeft(p.left+w); } if (p.top < window_min_y) { $(document).scrollTop(p.top); } else if (window_max_y < p.top+h) { $(document).scrollTop(p.top+h); } } // trigger event (once, cleared in keyup) if (!key_moving) { $('.glue-selected').not('.locked').trigger('glue-movestart'); key_moving = true; } // prevent window scrolling return false; } else if (e.ctrlKey && e.which == 65) { // select all objects not locked objects // selected locked objects will be unselected $('.object').not('.glue-selected').not('.locked').each(function() { $.glue.sel.select($(this)); }); // exclude locked objects from selection $('.locked.glue-selected').each(function() { $.glue.sel.deselect($(this)); }); return false; } else if (e.ctrlKey && e.which == 68) { // select none $.glue.sel.none(); return false; } else if (e.ctrlKey && e.which == 73) { // invert selection var next = $('.object').not('.glue-selected').not('.locked'); $.glue.sel.none(); $(next).each(function() { $.glue.sel.select($(this)); }); return false; } else { // DEBUG //console.log('html keydown '+e.which); } }); $('html').bind('keyup', function(e) { if (33 == e.which && e.shiftKey && $('.glue-selected').length) { // shift+pageup: move objects to top of stack $('.glue-selected').not('.locked').each(function() { $.glue.stack.to_top($(this)); $.glue.object.save($(this)); }); $.glue.stack.compress(); return false; } else if (34 == e.which && e.shiftKey && $('.glue-selected').length) { // shift+pagedown: move objects to bottom of stack $('.glue-selected').not('.locked').each(function() { $.glue.stack.to_bottom($(this)); $.glue.object.save($(this)); }); $.glue.stack.compress(); return false; } else if (37 <= e.which && e.which <= 40 && $('.glue-selected').length) { // move selected elements with arrow keys $('.glue-selected').not('.locked').trigger('glue-movestop'); key_moving = false; return false; } else if (e.which == 46 && $('.glue-selected').length) { // delete selected objects // this is pretty much copied from object-edit.js var objs = $('.glue-selected').not('.locked'); $(objs).each(function() { var id = $(this).attr('id'); $.glue.object.unregister($(this)); $(this).remove(); // delete in backend as well $.glue.backend({ method: 'glue.delete_object', name: id }); // update canvas $.glue.canvas.update(); }); return false; } else { // DEBUG //console.log('html keydown '+e.which); } }); $('.object').live('dragstart', function(e) { // contrain to axis when dragging with shift key pressed drag_start_x = $(this).position().left; drag_start_y = $(this).position().top; drag_mouse_start_x = e.pageX; drag_mouse_start_y = e.pageY; $(this).draggable('option', 'axis', false); if (!$(this).hasClass('glue-selected')) { // event for selected objects is triggered in the .glue-selected dragstart // handler $(this).trigger('glue-movestart'); } }); $('.object').live('dragstop', function(e) { // reset previous grid setting if (drag_prev_grid) { $(this).draggable('option', 'grid', drag_prev_grid); drag_prev_grid = false; } }); $('.object').live('drag', function(e) { // ignore grid when ctrl is pressed if (e.ctrlKey) { if ($(this).draggable('option', 'grid') !== false) { // save previous setting drag_prev_grid = $(this).draggable('option', 'grid'); // disable grid $(this).draggable('option', 'grid', false); } } else { // reset previous setting if (drag_prev_grid) { $(this).draggable('option', 'grid', drag_prev_grid); drag_prev_grid = false; } } // contrain to axis when dragging with shift key pressed if (e.shiftKey) { var dir; if (Math.abs(e.pageX-drag_mouse_start_x) < Math.abs(e.pageY-drag_mouse_start_y)) { dir = 'y'; } else { dir = 'x'; } var diff = Math.abs(Math.abs(e.pageX-drag_mouse_start_x)-Math.abs(e.pageY-drag_mouse_start_y)); if ($(this).draggable('option', 'axis') == false) { // move object back to the starting position $(this).css('left', drag_start_x+'px'); $(this).css('top', drag_start_y+'px'); $(this).draggable('option', 'axis', dir); } else { // only change direction if difference is greater than 50 pixels if (50 < diff && $(this).draggable('option', 'axis') != dir) { // move object back to the starting position $(this).css('left', drag_start_x+'px'); $(this).css('top', drag_start_y+'px'); $(this).draggable('option', 'axis', dir); } } } else { $(this).draggable('option', 'axis', false); } }); $('.object').live('dragstop', function(e) { if (!$(this).hasClass('glue-selected')) { // event for selected objects is triggered in the .glue-selected dragstop // handler $(this).trigger('glue-movestop'); } }); $('.glue-selected').live('drag', function(e) { if (1 < $('.glue-selected').length) { // dragging multiple selected object var that = this; var that_p = $(this).position(); $('.glue-selected').each(function() { if (this == that) { return; } var p = $(this).position(); $(this).css('left', (p.left+that_p.left-drag_prev_x)+'px'); $(this).css('top', (p.top+that_p.top-drag_prev_y)+'px'); }); drag_prev_x = that_p.left; drag_prev_y = that_p.top; } }); $('.glue-selected').live('dragstart', function(e) { if (1 < $('.glue-selected').length) { var p = $(this).position(); drag_prev_x = p.left; drag_prev_y = p.top; } $('.glue-selected').trigger('glue-movestart'); }); $('.glue-selected').live('dragstop', function(e) { // dragging multiple selected object // there does not seem to be a drag event for the position where the // mouse button is released, so the following is necessary var that = this; var that_p = $(this).position(); $('.glue-selected').each(function() { if (this == that) { return; } var p = $(this).position(); $(this).css('left', (p.left+that_p.left-drag_prev_x)+'px'); $(this).css('top', (p.top+that_p.top-drag_prev_y)+'px'); }); $('.glue-selected').trigger('glue-movestop'); }); $('.object').live('click', function(e) { // TODO (later): moving objects after shift clicking on them does not seem to work right on Chrome, document and fill a bug upstream if (!e.shiftKey && !$(this).hasClass('glue-selected')) { $.glue.sel.none(); } if (e.shiftKey && $(this).hasClass('glue-selected')) { $.glue.sel.deselect($(this)); } // shift clicking involving locked object will result in no action else if (e.shiftKey && $(this).hasClass('locked') || $('.glue-selected').hasClass('locked')) { return; } else { $.glue.sel.select($(this)); } }); $('.object').live('glue-movestop', function(e) { // update tooltip $.glue.object.resizable_update_tooltip(this); // save object $.glue.object.save(this); // update canvas $.glue.canvas.update(this); }); $('.object').live('glue-unregister', function(e) { $.glue.sel.deselect($(this)); }); return { // deselect an object // obj .. element deselect: function(obj) { if ($(obj).hasClass('glue-selected')) { var border = $(obj).outerHeight()-$(obj).innerHeight(); $(obj).removeClass('glue-selected'); $(obj).trigger('glue-deselect'); var p = $(obj).position(); $(obj).css('left', (p.left+border/2)+'px'); $(obj).css('top', (p.top+border/2)+'px'); //$(obj).css('width', ($(obj).width()+border)+'px'); //$(obj).css('height', ($(obj).height()+border)+'px'); // DEBUG //console.log('deselected '+$(obj).attr('id')); } }, // select none none: function() { $('.glue-selected').each(function() { $.glue.sel.deselect($(this)); }); }, // select an object // obj .. element select: function(obj) { // TODO (later): handle more than one obj (and change callers) if (!$(obj).hasClass('glue-selected')) { $(obj).addClass('glue-selected'); $(obj).trigger('glue-select'); // TODO (later): the following code works for dashed borders but // not for solid ones - read out the border-style on the fly and // act accordingly (there seem to be a problem with getting the // information through jQuery 1.4.3 however) // also needs changes above and in register_alter_pre_save var p = $(obj).position(); var border = $(obj).outerHeight()-$(obj).innerHeight(); $(obj).css('left', (p.left-border/2)+'px'); $(obj).css('top', (p.top-border/2)+'px'); //$(obj).css('width', ($(obj).width()-border)+'px'); //$(obj).css('height', ($(obj).height()-border)+'px'); // DEBUG //console.log('selected '+$(obj).attr('id')); } }, // return if an object is selected // obj .. element selected: function(obj) { return $(obj).hasClass('glue-selected'); } }; }(); $.glue.slider = function() { return function(e, change, stop) { var old_e = e; var mousemove = function(e) { if (typeof change == 'function') { change(e.pageX-old_e.pageX, e.pageY-old_e.pageY, e); } return false; }; var mouseup = function(e) { $('html').unbind('mousemove', mousemove); $('html').unbind('mouseup', mouseup); if (typeof change == 'function') { change(e.pageX-old_e.pageX, e.pageY-old_e.pageY, e); } if (typeof stop == 'function') { stop(e.pageX-old_e.pageX, e.pageY-old_e.pageY, e); } return false; }; $('html').bind('mousemove', mousemove); $('html').bind('mouseup', mouseup); }; }(); $.glue.stack = function() { var default_z = 100; var max_z = 199; var min_z = 0; var intersecting = function(a, b) { var a_h = $(a).outerHeight(); var a_p = $(a).position(); var a_w = $(a).outerWidth(); var b_h = $(b).outerHeight(); var b_p = $(b).position(); var b_w = $(b).outerWidth(); if ((a_p.left <= b_p.left+b_w && b_p.left <= a_p.left+a_w) && (a_p.top <= b_p.top+b_h && b_p.top <= a_p.top+a_h)) { return true; } else { return false; } }; return { compress: function() { var max = min_z-1; var min = max_z+1; var shift = 0; // get min and max z of all objects $('.object').not('.locked').each(function() { var z = parseInt($(this).css('z-index')); if (isNaN(z)) { return; } if (z < min) { min = z; } if (max < z) { max = z; } }); // compress levels for (var i=min; i<=max; i++) { // for each z-index level // check if there is an object in this level var found = false; $('.object').not('.locked').each(function() { var z = parseInt($(this).css('z-index')); if (isNaN(z)) { return; } else if (z == i) { found = true; } }); // if not, move all upper levels one down if (!found) { // DEBUG //console.log('compressing level '+i); max--; $('.object').not('.locked').each(function() { var z = parseInt($(this).css('z-index')); if (isNaN(z)) { return; } else if (i < z) { $(this).css('z-index', --z); $(this).addClass('need_save'); } }); } } // calculcate how much we want to shift all z's shift = default_z-Math.round((max-min)/2)-min; // DEBUG //console.log('shift is '+shift); if (Math.abs(shift) < 20) { shift = 0; } else { $('.object').addClass('need_save'); } // save objects $('.need_save').each(function() { var z = parseInt($(this).css('z-index')); if (!isNaN(z)) { $(this).css('z-index', z+shift); $.glue.object.save(this); } $(this).removeClass('need_save'); }); }, default_z: function() { return default_z; }, to_bottom: function(obj) { var local_min_z = max_z+1; var old_z = parseInt($(obj).css('z-index')); $('.object').not('.locked').each(function() { if (this == $(obj).get(0)) { return; } if (!intersecting(obj, this)) { return; } else { // DEBUG //console.log('object intersects '+$(this).attr('id')); } if ($(this).css('z-index').length) { var z = parseInt($(this).css('z-index')); if (!isNaN(z) && z < local_min_z) { local_min_z = z; } } }); // check if we need to update the object if (isNaN(old_z) || local_min_z <= old_z) { // check if we really found an intersecting element (otherwise // local_min_z is max_z+1) and if we are inside min_z if (local_min_z <= max_z && min_z < local_min_z) { $(obj).css('z-index', local_min_z-1); // DEBUG //console.log('set z-index to '+(local_min_z-1)); return true; } } return false; }, to_top: function(obj) { var local_max_z = min_z-1; var old_z = parseInt($(obj).css('z-index')); $('.object').not('.locked').each(function() { if (this == $(obj).get(0)) { return; } if (!intersecting(obj, this)) { return; } else { // DEBUG //console.log('object intersects '+$(this).attr('id')); } if ($(this).css('z-index').length) { var z = parseInt($(this).css('z-index')); if (!isNaN(z) && local_max_z < z) { local_max_z = z; } } }); // check if we need to update the object if (isNaN(old_z) || old_z <= local_max_z) { // check if we really found an intersecting element (otherwise // local_max_z is min_z-1) and if we are inside max_z if (min_z <= local_max_z && local_max_z < max_z) { $(obj).css('z-index', local_max_z+1); // DEBUG //console.log('set z-index to '+(local_max_z+1)); return true; } } return false; } }; }(); $.glue.upload = function() { // helper function that provides a default upload // orig_x .. (page) x position of upload (can be set on the fly in .x) // orig_y .. (page) y position of upload (can be set on the fly in .y) // TODO (later): expose this through $.glue.upload.default_upload_handling var default_upload_handling = function(orig_x, orig_y) { if (orig_x === undefined) { orig_x = 0; } if (orig_y === undefined) { orig_y = 0; } var uploading = 0; return { error: function(e) { // remove status indicator if no file uploading anymore uploading--; if (uploading == 0) { $(this.status).detach(); } // e.target.status suggested in // http://developer.mozilla.org/en/XMLHttpRequest/Using_XMLHttpRequest if (e && e.target && e.target.status) { $.glue.error('There was a problem uploading a file (status '+e.target.status+')'); } else { $.glue.error('There was a problem uploading a file. Make sure you are not exceeding the file size limits set in the server configuration.'); // DEBUG console.error(e); } }, finish: function(data) { // DEBUG //console.log('finished uploading'); // remove status indicator if no file uploading anymore uploading--; if (uploading == 0) { // DEBUG //console.log('no files uploading anymore, removing status indicator'); $(this.status).detach(); } // handle response $.glue.upload.handle_response(data, this.x, this.y); }, progress: function(e) { // update status indicator // TODO (later): values are off on Chrome when uploading multiple file, one after another (it jumps back and forth) (report) $(this.status).children('.glue-upload-statusbar-done').css('width', (e.loaded/e.total*100)+'%'); $(this.status).attr('title', e.loaded+' of '+e.total+' bytes ('+(e.loaded/e.total*100).toFixed(1)+'%)'); }, start: function(e) { // DEBUG //console.log('started uploading'); $.glue.menu.hide(); uploading++; // add status indicator to dom $('body').append(this.status); $(this.status).children('.glue-upload-statusbar-done').css('width', '0%'); $(this.status).css('left', (this.x-$(this.status).outerWidth()/2)+'px'); $(this.status).css('top', (this.y-$(this.status).outerHeight()/2)+'px'); }, status: $('
'), x: orig_x, y: orig_y } }; $(document).ready(function() { // generic upload button var elem = $('
btn
'); var upload = default_upload_handling(); upload.multiple = true; $.glue.upload.button(elem, { method: 'glue.upload_files', page: $.glue.page }, upload); $(elem).bind('click', function(e) { // update x, y var p = $.glue.menu.spawn_coords(); upload.x = p.x; upload.y = p.y; }); $.glue.menu.register('new', elem, 11); // handle drop events on body // this is based on http://developer.mozilla.org/en/using_files_from_web_applications // does not seem to be possible in jQuery at the moment // we use html here as body doesn't get enlarged when zooming out e.g. $('html').get(0).addEventListener('dragover', function(e) { e.stopPropagation(); e.preventDefault(); }, false); $('html').get(0).addEventListener('drop', function(e) { e.stopPropagation(); e.preventDefault(); // pageX, pageY are available in Firefox and Chrome // TODO (later): pageX, pageY does not seem to handle zoomed pages in Chrome (report) var upload = default_upload_handling(e.pageX, e.pageY); $.glue.upload.files(e.dataTransfer.files, { method: 'glue.upload_files', page: $.glue.page }, upload); }, false); }); return { // elem .. element to turn into a file button // data .. other parameters to send to the service // options .. multiple => allow multiple files to be uploaded (boolean, defaults to false) // tooltip => title attribute on the file button // abort => function called if the upload didn't start // start => function called when the upload started // progress => function called periodically during the upload // error => function called when an error occured // finish => function called after the upload has completed button: function(elem, data, options) { // add a file input to the element if (!options) { options = {}; } if (!options.tooltip) { options.tooltip = 'upload a file'; } $(elem).prepend(''); if (options.multiple) { $(elem).children('input').first().attr('multiple', 'multiple'); } // add event handler $(elem).children('input').first().bind('change', function(e) { if (!this.files || this.files.length == 0) { if (typeof options.abort == 'function') { options.abort(); } return false; } else { $.glue.upload.files(this.files, data, options); return false; } }); }, // files .. array of file-objects (see $.glue.upload.button) // data .. other parameters to send to the service // options .. abort => function called if the upload didn't start // start => function called when the upload started // progress => function called periodically during the upload // error => function called when an error occured // finish => function called after the upload has completed files: function(files, data, options) { // based on http://www.appelsiini.net/2009/10/html5-drag-and-drop-multiple-file-upload // and jquery-html5-upload if (!data) { data = {}; } if (!options) { options = {}; } var xhr = new XMLHttpRequest(); if (typeof options.progress == 'function') { // this is needed otherwise this is XMLHttpRequestUpload in the // progress handler xhr.upload['onprogress'] = function(e) { options.progress(e); } } if (typeof options.finish == 'function') { xhr.onload = function(e) { try { options.finish($.parseJSON(e.target.responseText)); } catch (e) { if (typeof options.error == 'function') { options.error(e); } } }; } if (typeof options.error == 'function') { xhr.onerror = function(e) { options.error(e); } } xhr.open('POST', $.glue.base_url+'json.php', true); if (window.FormData) { // DEBUG //console.log('upload: using FormData'); var f = new FormData(); // other parameters for (var key in data) { f.append(key, JSON.stringify(data[key])); } // files for (var i=0; i < files.length; i++) { f.append('user_file'+i, files[i]); } xhr.send(f); if (typeof options.start == 'function') { options.start(files); } return true; } else if (files[0] && files[0].getAsBinary) { // DEBUG //console.log('upload: using getAsBinary'); // build RFC2388 string var boundary = '----multipartformboundary'+(new Date).getTime(); var builder = ''; // other parameters for (var key in data) { builder += '--'+boundary+'\r\n'; builder += 'Content-Disposition: form-data; name="'+key+'"'+'\r\n'; builder += '\r\n'; builder += JSON.stringify(data[key])+'\r\n'; } // files for (var i=0; i < files.length; i++) { var file = files[i]; builder += '--'+boundary+'\r\n'; builder += 'Content-Disposition: form-data; name="user_file'+i+'"'; if (file.fileName) { builder += '; filename="'+file.fileName+'"'; } builder += '\r\n'; if (file.type) { builder += 'Content-Type: '+file.type+'\r\n'; } else { builder += 'Content-Type: application/octet-stream'+'\r\n'; } builder += '\r\n'; builder += file.getAsBinary(); builder += '\r\n'; } // mark end of request builder += '--'+boundary+'--'+'\r\n'; xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary='+boundary); xhr.sendAsBinary(builder); if (typeof options.start == 'function') { options.start(files); } return true; } else { $.glue.error('Your browser is not supported. Update to a recent version of Firefox or Chrome.'); if (typeof options.abort == 'function') { options.abort(); } return false; } }, handle_response: function(data, x, y) { if (!data) { $.glue.error('There was a problem communicating with the server'); } else if (data['#error']) { $.glue.error('There was a problem uploading the file ('+data['#data']+')'); } else { // add new elements to the dom and register them if (data['#data'].length == 0) { // special case for no new elements $.glue.error('The server did not reply with any object. The file type you were uploading could either not be supported (look around for more modules!) or there could be an internal problem. Check the log file to be sure!'); return; } // we're not selecting the new objects but at least clear the current selection $.glue.sel.none(); for (var i=0; i < data['#data'].length; i++) { var obj = $(data['#data'][i]); // load event handler var content_loaded = function(e) { // function scope bites us in the ass here var mode = e.data.mode; var target_x = e.data.target_x; var target_y = e.data.target_y; if ($(this).hasClass('object')) { var obj = $(this); } else { var obj = $(this).parents('.object').first(); } // set default width and height $(obj).css('width', $(obj).width()+'px'); $(obj).css('height', $(obj).height()+'px'); // DEBUG //console.log('glue-upload-dynamic-late: '+$(obj).attr('id')); // fire handler (can overwrite width and height) $(obj).trigger('glue-upload-dynamic-late', [ this ]); // position object if (mode == 'center') { // move to the center of mouseclick $(obj).css('left', (target_x-$(obj).outerWidth()/2)+'px'); $(obj).css('top', (target_y-$(obj).outerHeight()/2)+'px'); } else { // move to stack $(obj).css('left', (target_x+'px')); $(obj).css('top', (target_y+'px')); } // restore visibility $(obj).css('visibility', $(obj).data('orig_visibility')); $(obj).removeData('orig_visibility'); // register object $.glue.object.register(obj); // save object $.glue.object.save(obj); } // set mode and target x, y if (data['#data'].length == 1) { var mode = 'center'; var target_x = x; var target_y = y; } else { var mode = 'stack'; var target_x = x+i*$.glue.grid.x(); var target_y = y+i*$.glue.grid.y(); } // check if we have dimensions already var width = parseInt($(obj).get(0).style.getPropertyValue('width')); if (isNaN(width) || width === 0) { // bind load event handlers $(obj).bind('load', { 'mode': mode, 'target_x': target_x, 'target_y': target_y }, content_loaded); $(obj).find('*').bind('load', { 'mode': mode, 'target_x': target_x, 'target_y': target_y }, content_loaded); // save initial visibility and make object invisible $(obj).data('orig_visibility', $(obj).css('visibility')); $(obj).css('visibility', 'hidden'); // add to dom $('body').append(obj); // DEBUG //console.log('glue-upload-dynamic-early: '+$(obj).attr('id')); // fire handler $(obj).trigger('glue-upload-dynamic-early', [ mode, target_x, target_y ]); } else { // add to dom $('body').append(obj); // position object if (mode == 'center') { // move to the center of mouseclick $(obj).css('left', (target_x-$(obj).outerWidth()/2)+'px'); $(obj).css('top', (target_y-$(obj).outerHeight()/2)+'px'); } else { // move to stack $(obj).css('left', (target_x+'px')); $(obj).css('top', (target_y+'px')); } // register object $.glue.object.register(obj); // DEBUG //console.log('registered static upload: '+$(obj).attr('id')); // fire handler $(obj).trigger('glue-upload-static', [ mode ]); // save object $.glue.object.save(obj); } } } } }; }(); $(document).ready(function() { // register all objects $('.object').each(function() { $.glue.object.register($(this)); }); // make sure we call enlarge body even if there are no objects $.glue.canvas.update(); // enlarge body when we resize the window var resize_timer; $(window).bind('resize', function(e) { clearTimeout(resize_timer); resize_timer = setTimeout(function() { $.glue.canvas.update(); }, 100); }); // trigger menus on click and doubleclick var menu_dblclick_timeout = false; $('html').bind('click', function(e) { // make sure no iframe has focus as this breaks keyboard shortcuts etc window.focus(); // we use 'html' here to give the colorpicker et al a chance to stop the // propagation of the event in 'body' if (e.target == $('body').get(0)) { if (!$.glue.menu.is_shown()) { if (menu_dblclick_timeout) { clearTimeout(menu_dblclick_timeout); menu_dblclick_timeout = false; // show page menu $.glue.menu.show('page', e.clientX, e.clientY); return false; } menu_dblclick_timeout = setTimeout(function() { menu_dblclick_timeout = false; // prevent the new menu from showing when the user wants to // simply clear any open menu if ($.glue.menu.prev_menu() == '') { // show new menu $.glue.menu.show('new', e.clientX, e.clientY); } }, 300); } } }); // menu shortcuts $('html').bind('keyup', function(e) { if (e.altKey && e.which == 79) { // alt+o: show new object menu $.glue.menu.show('new'); return false; } else if (e.altKey && e.which == 80) { // alt+p: show page menu $.glue.menu.show('page'); return false; } else if (e.ctrlKey && e.which == 90) { // ctrl+z: show revisions browser to suggest using revisions in place of undo if (confirm('Looking for an "undo" option?\nHOTGLUE keeps record of your recent edits - it\'s called "revisions".\nWould you like to browse through the revisions of this page?')) { window.location = $.glue.base_url+'?'+$.glue.page+'/revisions'; return false; } } }); // I really don't know why, but when we don't handle the mousedown event here // double-clicking the page does select some object (the first child of body // on Firefox and the nearest element on Chrome) $('html').bind('mousedown', function(e) { return false; }); });