Q/columns tool
Let's make a tool that will form the core of the Groups user experience. It will be the tool that the user interacts with the most when they are navigating the app.
And it will work on both desktop and mobile.
Here is how you make a tool: http://platform.qbix.com/guide/tools
Basically, the idea is to manage one or more columns, in which we will display content. On the desktop and tablet environments, imagine a dark gray desktop with some sheets of white paper (maybe with some shadow). Some areas in the sheet of paper will be scrollable -- the ones with overflow: auto. Each column will be a div with a .Q_column class, allowing you to style it in CSS. You can use the html.Q_notTouchscreen .Q_column selectors to match the desktop environment, and html.Q_touchscreen.Q_notMobile .Q_column selectors to match the tablet environment, or html.Q_notMobile .Q_column to match both.
Please have a scrollbarsAutoHide: {} option for this tool, which will provide options to the Q/scrollbarsAutoHide jQuery plugin that the Q/columns tool will call on all the overflow: auto areas when it makes a new column. You can see the effect at http://platform.qbix.com -- it appears only on the desktop, but it lets you see only one scrollbar at a time, when you mouse over. This is to prevent lots of distracting scrollbars from appearing.
Column indexes will go from 0 for the first column, increasing by one for each column.
On mobile, only one column at a time will be displayed. I will now describe the methods for the tool, which also explain how to add/remove columns as well as switch between them on a mobile device.
-
count()- this will return the number of columns that are currently being shown -
open(url, slotName [, index] [, options])will create a new<div class="Q_column" data-index="3">element ifindexis undefined or is =count(), otherwise it will replace html in an existing column's div. The method should throw an error ifindex > count(). You should useQ.loadUrlwith the providedurlandslotNamebut also supplyslotContainerobject to map theslotNameto the right div to fill. You should passQ.extend({url: ..., slotName: ..., slotContainer: ...}, options)toQ.loadUrloptions. I thinkQ.loadUrlcallsQ.activateautomatically after filling that div, so your job will be almost done. In addition to passing the options toQ.loadUrl, there will be an additional option for theopenmethod calledtrigger, which expects a reference to anElement. See theonOpenandbeforeCloseevents below. -
close([index])closes that particular column, if index is omitted then it should be =count() - 1. -
column(index)- a reference to the div corresponding to that column -
trigger(index)- a reference to anElementpassed as thetriggeroption toopenmethod, above, for that column, or null if nothing was passed
The tool should also accept the following default options:
{
onOpen: new Q.Event(function () { ... }, 'Q/columns'},
beforeClose: new Q.Event(function () { ... }, 'Q/columns'},
animation: {
duration: 500 // milliseconds
}
}
these are events with handlers added by default, under the Q/columns key. These events should be handled from open and close functions, as in the following code:
open: function (url, slotName, index, options) {
if (index > this.count()) throw new Q.Exception("Q/columns open: index is too big");
var div = this.column(index);
if (!div) {
div = document.createElement('div').addClass('Q_column');
this.element.appendChild(div); // trying to avoid jQuery just for practice
++this.state.count;
this.state.columns[index] = div;
}
var slotContainer = {}; slotContainer[slotName] = div;
var o = Q.extend({
slotNames: [slotName],
slotContainer: slotContainer
}, options);
o.onActivate = function _onActivate() {
this.onOpen.handle(url, slotName, index, options);
}
this.state.triggers[index] = options.trigger || null;
Q.loadUrl(url, o);
},
close: function (index) {
var div = this.column(index);
if (!div) throw new Q.Exception("Column with index " + index + " doesn't exist");
var shouldContinue = this.beforeClose.handle();
if (shouldContinue === false) return;
// this is the proper way to remove an element, so tools inside can be destructed:
Q.removeElement(this.column(index));
},
column: function (index) { return this.columns[index]; },
trigger: function (index) { return this.triggers[index]; }
finally, use the Q.Animation class to implement those default handlers for onOpen and onClose events. All the animations should complete in a total of this.state.duration milliseconds. Something like this:
onOpen:
var div = this.column(index);
var $trigger = $(trigger);
var offset = $trigger.offset();
var cloned = trigger.cloneNode(true).copyComputedStyle(trigger);
$(div).css({
style: 0,
position: 'absolute',
left: offset.left,
top: offset.top,
width: $trigger.width(),
height: $trigger.height(),
zIndex: $trigger.css('zIndex')
});
Q.Animation.play(callback, this.state.animation.duration, this.state.animation.ease, params);
basically the onOpen animation should animate the column's opacity from 0 to 1 as it animates its left, top, width and height from overlapping the trigger to its final position ... see the diagram for its position in desktop and tablet vs mobile.
And beforeClose animation should do the reverse, moving it back to the trigger.
However, if the trigger is completely off screen, or not in the DOM, or is null, then just animate the opacity from without moving the column, and then remove the column in the close method, after beforeClose finishes.
