From 816de0918c28461cc2d1e3457348fd5b6e11950f Mon Sep 17 00:00:00 2001 From: Lionel LASKE Date: Tue, 18 Sep 2012 19:16:20 +0000 Subject: Initial version --- (limited to 'html/lib/layout') diff --git a/html/lib/layout/fittable/package.js b/html/lib/layout/fittable/package.js new file mode 100644 index 0000000..3fae95b --- /dev/null +++ b/html/lib/layout/fittable/package.js @@ -0,0 +1,3 @@ +enyo.depends( + "source" +); \ No newline at end of file diff --git a/html/lib/layout/fittable/source/FittableColumns.js b/html/lib/layout/fittable/source/FittableColumns.js new file mode 100644 index 0000000..d28b6fe --- /dev/null +++ b/html/lib/layout/fittable/source/FittableColumns.js @@ -0,0 +1,41 @@ +/** + _enyo.FittableColumns_ provides a container in which items are laid out in a + set of vertical columns, with most items having natural size, but one + expanding to fill the remaining space. The one that expands is labeled with + the attribute _fit: true_. + + For example, the following code will align three components as columns, with + the second filling the available container space between the first and third: + + enyo.kind({ + kind: "FittableColumns", + components: [ + {content: "1"}, + {content: "2", fit:true}, + {content: "3"} + ] + }); + + Alternatively, you may set a kind's _layoutKind_ property to + enyo.FittableColumnsLayout + to use a different base kind while still employing the fittable layout + strategy, e.g.: + + enyo.kind({ + kind: enyo.Control, + layoutKind: "FittableColumnsLayout", + components: [ + {content: "1"}, + {content: "2", fit:true}, + {content: "3"} + ] + }); +*/ + +enyo.kind({ + name: "enyo.FittableColumns", + layoutKind: "FittableColumnsLayout", + /** By default, items in columns stretch to fit vertically; set to true to + avoid this behavior. */ + noStretch: false +}); diff --git a/html/lib/layout/fittable/source/FittableLayout.css b/html/lib/layout/fittable/source/FittableLayout.css new file mode 100644 index 0000000..cdc24b8 --- /dev/null +++ b/html/lib/layout/fittable/source/FittableLayout.css @@ -0,0 +1,69 @@ +.enyo-fittable-rows-layout { + position: relative; +} + +.enyo-fittable-rows-layout > * { + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + /* float when not stretched */ + float: left; + clear: both; +} + +/* non-floating when stretched */ +.enyo-fittable-rows-layout.enyo-stretch > * { + float: none; + clear: none; +} + +/* setting to enforce margin collapsing */ +/* NOTE: rows cannot have margin left/right */ +.enyo-fittable-rows-layout.enyo-stretch.enyo-margin-expand > * { + float: left; + clear: both; + width: 100%; + /* note: harsh resets */ + margin-left: 0 !important; + margin-right: 0 !important; +} + +.enyo-fittable-columns-layout { + position: relative; + text-align: left; + white-space: nowrap; +} + +.enyo-fittable-columns-layout.enyo-center { + text-align: center; +} + +.enyo-fittable-columns-layout > * { + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + vertical-align: top; + display: inline-block; + white-space: normal; +} + +.enyo-fittable-columns-layout.enyo-tool-decorator > * { + vertical-align: middle; +} + +/* repair clobbered white-space setting for pre, code */ +.enyo-fittable-columns-layout > pre, .enyo-fittable-columns-layout > code { + white-space: pre; +} + +.enyo-fittable-columns-layout > .enyo-fittable-columns-layout, .enyo-fittable-columns-layout > .onyx-toolbar-inline { + white-space: nowrap; +} + +/* NOTE: columns cannot have margin top/bottom */ +.enyo-fittable-columns-layout.enyo-stretch > * { + height: 100%; + /* note: harsh resets */ + margin-top: 0 !important; + margin-bottom: 0 !important; +} \ No newline at end of file diff --git a/html/lib/layout/fittable/source/FittableLayout.js b/html/lib/layout/fittable/source/FittableLayout.js new file mode 100644 index 0000000..6045e65 --- /dev/null +++ b/html/lib/layout/fittable/source/FittableLayout.js @@ -0,0 +1,265 @@ +/** + _enyo.FittableLayout_ provides the base positioning and boundary logic for + the fittable layout strategy. The fittable layout strategy is based on + laying out items in either a set of rows or a set of columns, with most of + the items having natural size, but one item expanding to fill the remaining + space. The item that expands is labeled with the attribute _fit: true_. + + For example, in the following kind, the second component fills the available + space in the container between the first and third components. + + enyo.kind({ + kind: "FittableRows", + components: [ + {content: "1"}, + {content: "2", fit:true}, + {content: "3"} + ] + }); + + enyo.FittableColumnsLayout and + enyo.FittableRowsLayout (or their + subkinds) are used for layout rather than _enyo.FittableLayout_ because they + specify properties that _enyo.FittableLayout_ expects to be available when + laying items out. +*/ +enyo.kind({ + name: "enyo.FittableLayout", + kind: "Layout", + //* @protected + calcFitIndex: function() { + for (var i=0, c$=this.container.children, c; c=c$[i]; i++) { + if (c.fit && c.showing) { + return i; + } + } + }, + getFitControl: function() { + var c$=this.container.children; + var f = c$[this.fitIndex]; + if (!(f && f.fit && f.showing)) { + this.fitIndex = this.calcFitIndex(); + f = c$[this.fitIndex]; + } + return f; + }, + getLastControl: function() { + var c$=this.container.children; + var i = c$.length-1; + var c = c$[i]; + while ((c=c$[i]) && !c.showing) { + i--; + } + return c; + }, + _reflow: function(measure, cMeasure, mAttr, nAttr) { + this.container.addRemoveClass("enyo-stretch", !this.container.noStretch); + var f = this.getFitControl(); + // no sizing if nothing is fit. + if (!f) { + return; + } + // + // determine container size, available space + var s=0, a=0, b=0, p; + var n = this.container.hasNode(); + // calculate available space + if (n) { + // measure 1 + p = enyo.FittableLayout.calcPaddingExtents(n); + // measure 2 + s = n[cMeasure] - (p[mAttr] + p[nAttr]); + //console.log("overall size", s); + } + // + // calculate space above fitting control + // measure 3 + var fb = f.getBounds(); + // offset - container padding. + a = fb[mAttr] - ((p && p[mAttr]) || 0); + //console.log("above", a); + // + // calculate space below fitting control + var l = this.getLastControl(); + if (l) { + // measure 4 + var mb = enyo.FittableLayout.getComputedStyleValue(l.hasNode(), "margin", nAttr) || 0; + if (l != f) { + // measure 5 + var lb = l.getBounds(); + // fit offset + size + var bf = fb[mAttr] + fb[measure]; + // last offset + size + ending margin + var bl = lb[mAttr] + lb[measure] + mb; + // space below is bottom of last item - bottom of fit item. + b = bl - bf; + } else { + b = mb; + } + } + + // calculate appropriate size for fit control + var fs = s - (a + b); + //console.log(f.id, fs); + // note: must be border-box; + f.applyStyle(measure, fs + "px"); + }, + //* @public + /** + Updates the layout to reflect any changes to contained components or the + layout container. + */ + reflow: function() { + if (this.orient == "h") { + this._reflow("width", "clientWidth", "left", "right"); + } else { + this._reflow("height", "clientHeight", "top", "bottom"); + } + }, + statics: { + //* @protected + _ieCssToPixelValue: function(inNode, inValue) { + var v = inValue; + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + var s = inNode.style; + // store style and runtime style values + var l = s.left; + var rl = inNode.runtimeStyle && inNode.runtimeStyle.left; + // then put current style in runtime style. + if (rl) { + inNode.runtimeStyle.left = inNode.currentStyle.left; + } + // apply given value and measure its pixel value + s.left = v; + v = s.pixelLeft; + // finally restore previous state + s.left = l; + if (rl) { + s.runtimeStyle.left = rl; + } + return v; + }, + _pxMatch: /px/i, + getComputedStyleValue: function(inNode, inProp, inBoundary, inComputedStyle) { + var s = inComputedStyle || enyo.dom.getComputedStyle(inNode); + if (s) { + return parseInt(s.getPropertyValue(inProp + "-" + inBoundary)); + } else if (inNode && inNode.currentStyle) { + var v = inNode.currentStyle[inProp + enyo.cap(inBoundary)]; + if (!v.match(this._pxMatch)) { + v = this._ieCssToPixelValue(inNode, v); + } + return parseInt(v); + } + return 0; + }, + //* Gets the boundaries of a node's margin or padding box. + calcBoxExtents: function(inNode, inBox) { + var s = enyo.dom.getComputedStyle(inNode); + return { + top: this.getComputedStyleValue(inNode, inBox, "top", s), + right: this.getComputedStyleValue(inNode, inBox, "right", s), + bottom: this.getComputedStyleValue(inNode, inBox, "bottom", s), + left: this.getComputedStyleValue(inNode, inBox, "left", s) + }; + }, + //* Gets the calculated padding of a node. + calcPaddingExtents: function(inNode) { + return this.calcBoxExtents(inNode, "padding"); + }, + //* Gets the calculated margin of a node. + calcMarginExtents: function(inNode) { + return this.calcBoxExtents(inNode, "margin"); + } + } +}); + +/** + _enyo.FittableColumnsLayout_ provides a container in which items are laid + out in a set of vertical columns, with most of the items having natural + size, but one expanding to fill the remaining space. The one that expands is + labeled with the attribute _fit: true_. + + _enyo.FittableColumnsLayout_ is meant to be used as a value for the + _layoutKind_ property of other kinds. _layoutKind_ provides a way to add + layout behavior in a pluggable fashion while retaining the ability to use a + specific base kind. + + For example, the following code will align three components as columns, with + the second filling the available container space between the first and third. + + enyo.kind({ + kind: enyo.Control, + layoutKind: "FittableColumnsLayout", + components: [ + {content: "1"}, + {content: "2", fit:true}, + {content: "3"} + ] + }); + + Alternatively, if a specific base kind is not needed, then instead of + setting the _layoutKind_ attribute, you can set the base kind to + enyo.FittableColumns: + + enyo.kind({ + kind: "FittableColumns", + components: [ + {content: "1"}, + {content: "2", fit:true}, + {content: "3"} + ] + }); +*/ +enyo.kind({ + name: "enyo.FittableColumnsLayout", + kind: "FittableLayout", + orient: "h", + layoutClass: "enyo-fittable-columns-layout" +}); + + +/** + _enyo.FittableRowsLayout_ provides a container in which items are laid out + in a set of horizontal rows, with most of the items having natural size, but + one expanding to fill the remaining space. The one that expands is labeled + with the attribute _fit: true_. + + _enyo.FittableRowsLayout_ is meant to be used as a value for the + _layoutKind_ property of other kinds. _layoutKind_ provides a way to add + layout behavior in a pluggable fashion while retaining the ability to use a + specific base kind. + + For example, the following code will align three components as rows, with + the second filling the available container space between the first and third. + + enyo.kind({ + kind: enyo.Control, + layoutKind: "FittableRowsLayout", + components: [ + {content: "1"}, + {content: "2", fit:true}, + {content: "3"} + ] + }); + + Alternatively, if a specific base kind is not needed, then instead of + setting the _layoutKind_ attribute, you can set the base kind to + enyo.FittableRows: + + enyo.kind({ + kind: "FittableRows", + components: [ + {content: "1"}, + {content: "2", fit:true}, + {content: "3"} + ] + }); +*/ +enyo.kind({ + name: "enyo.FittableRowsLayout", + kind: "FittableLayout", + layoutClass: "enyo-fittable-rows-layout", + orient: "v" +}); diff --git a/html/lib/layout/fittable/source/FittableRows.js b/html/lib/layout/fittable/source/FittableRows.js new file mode 100644 index 0000000..985f82d --- /dev/null +++ b/html/lib/layout/fittable/source/FittableRows.js @@ -0,0 +1,40 @@ +/** + _enyo.FittableRows_ provides a container in which items are laid out in a + set of horizontal rows, with most of the items having natural size, but one + expanding to fill the remaining space. The one that expands is labeled with + the attribute _fit: true_. + + For example, the following code will align three components as rows, with + the second filling the available container space between the first and third. + + enyo.kind({ + kind: "FittableRows", + components: [ + {content: "1"}, + {content: "2", fit:true}, + {content: "3"} + ] + }); + + Alternatively, you may set a kind's _layoutKind_ property to + enyo.FittableRowsLayout + to use a different base kind while still employing the fittable layout + strategy, e.g.: + + enyo.kind({ + kind: enyo.Control, + layoutKind: "FittableRowsLayout", + components: [ + {content: "1"}, + {content: "2", fit:true}, + {content: "3"} + ] + }); +*/ +enyo.kind({ + name: "enyo.FittableRows", + layoutKind: "FittableRowsLayout", + /** By default, items in rows stretch to fit horizontally; set to true to + avoid this behavior. */ + noStretch: false +}); diff --git a/html/lib/layout/fittable/source/design.js b/html/lib/layout/fittable/source/design.js new file mode 100644 index 0000000..ab94505 --- /dev/null +++ b/html/lib/layout/fittable/source/design.js @@ -0,0 +1,23 @@ +/** + Description to make fittable kinds available in Ares. +*/ +Palette.model.push( + {name: "fittable", items: [ + {name: "FittableRows", title: "Vertical stacked layout", icon: "package_new.png", stars: 4.5, version: 2.0, blurb: "Stack of vertical rows, one of which can be made to fit.", + inline: {kind: "FittableRows", style: "height: 80px; position: relative;", padding: 4, components: [ + {style: "background-color: lightblue; border: 1px dotted blue; height: 15px;"}, + {style: "background-color: lightblue; border: 1px dotted blue;", fit: true}, + {style: "background-color: lightblue; border: 1px dotted blue; height: 15px;"} + ]}, + config: {content: "$name", isContainer: true, kind: "FittableRows", padding: 10, margin: 10} + }, + {name: "FittableColumns", title: "Horizontal stacked layout", icon: "package_new.png", stars: 4.5, version: 2.0, blurb: "Stack of horizontal columns, one of which can be made to fit.", + inline: {kind: "FittableColumns", style: "height: 60px; position: relative;", padding: 4, components: [ + {style: "background-color: lightblue; border: 1px dotted blue; width: 20px;"}, + {style: "background-color: lightblue; border: 1px dotted blue;", fit: true}, + {style: "background-color: lightblue; border: 1px dotted blue; width: 20px;"} + ]}, + config: {content: "$name", isContainer: true, kind: "FittableColumns", padding: 10, margin: 10} + } + ]} +); \ No newline at end of file diff --git a/html/lib/layout/fittable/source/package.js b/html/lib/layout/fittable/source/package.js new file mode 100644 index 0000000..74201af --- /dev/null +++ b/html/lib/layout/fittable/source/package.js @@ -0,0 +1,6 @@ +enyo.depends( + "FittableLayout.css", + "FittableLayout.js", + "FittableRows.js", + "FittableColumns.js" +); \ No newline at end of file diff --git a/html/lib/layout/list/package.js b/html/lib/layout/list/package.js new file mode 100644 index 0000000..f1de4f2 --- /dev/null +++ b/html/lib/layout/list/package.js @@ -0,0 +1,3 @@ +enyo.depends( + "source/" +); \ No newline at end of file diff --git a/html/lib/layout/list/source/AlphaJumpList.js b/html/lib/layout/list/source/AlphaJumpList.js new file mode 100644 index 0000000..6b6e326 --- /dev/null +++ b/html/lib/layout/list/source/AlphaJumpList.js @@ -0,0 +1,37 @@ +/** + A control that presents an alphabetic panel that you can select from, in + order to perform actions based on the item selected. + + {kind: "AlphaJumpList", onSetupItem: "setupItem", + onAlphaJump: "alphaJump", + components: [ + {name: "divider"}, + {kind: "onyx.Item"} + ] + } + +*/ +enyo.kind({ + name: "enyo.AlphaJumpList", + kind: "List", + //* @protected + scrollTools: [ + {name: "jumper", kind: "AlphaJumper"} + ], + initComponents: function() { + this.createChrome(this.scrollTools); + this.inherited(arguments); + }, + rendered: function() { + this.inherited(arguments); + this.centerJumper(); + }, + resizeHandler: function() { + this.inherited(arguments); + this.centerJumper(); + }, + centerJumper: function() { + var b = this.getBounds(), sb = this.$.jumper.getBounds(); + this.$.jumper.applyStyle("top", ((b.height - sb.height) / 2) + "px"); + } +}); \ No newline at end of file diff --git a/html/lib/layout/list/source/AlphaJumper.css b/html/lib/layout/list/source/AlphaJumper.css new file mode 100644 index 0000000..df1265c --- /dev/null +++ b/html/lib/layout/list/source/AlphaJumper.css @@ -0,0 +1,36 @@ +.enyo-alpha-jumper { + text-transform: uppercase; + font-size: 11px; + xborder-radius: 12px; + position: absolute; + xbackground: white; + text-align: center; + right: 10px; + z-index: 1; + color: #333; + padding: 0 14px; +} + +.enyo-alpha-jumper > *:first-child { + padding-top: 4px; + border-radius: 12px 12px 0 0; + border-top: 1px solid #333; +} + +.enyo-alpha-jumper > *:last-child { + padding-bottom: 4px; + border-radius: 0 0 12px 12px; + border-bottom: 1px solid #333; +} + +.enyo-alpha-jumper > * { + background: #eee; + padding: 1px 4px; + border-right: 1px solid #333; + border-left: 1px solid #333; +} + +.enyo-alpha-jumper > .active { + background: #333; + color: white; +} \ No newline at end of file diff --git a/html/lib/layout/list/source/AlphaJumper.js b/html/lib/layout/list/source/AlphaJumper.js new file mode 100644 index 0000000..2f85d00 --- /dev/null +++ b/html/lib/layout/list/source/AlphaJumper.js @@ -0,0 +1,70 @@ +enyo.kind({ + name: "enyo.AlphaJumper", + classes: "enyo-alpha-jumper enyo-border-box", + published: { + marker: null + }, + events: { + onAlphaJump: "" + }, + handlers: { + ondown: "down", + onmove: "move", + onup: "up" + }, + initComponents: function() { + for (var s="A".charCodeAt(0), i=s; i, selected: }_ + */ + onSetupItem: "" + }, + components: [ + {kind: "Selection", onSelect: "selectDeselect", onDeselect: "selectDeselect"}, + {name: "client"} + ], + rowOffset: 0, + bottomUp: false, + //* @protected + create: function() { + this.inherited(arguments); + this.multiSelectChanged(); + }, + multiSelectChanged: function() { + this.$.selection.setMulti(this.multiSelect); + }, + setupItem: function(inIndex) { + this.doSetupItem({index: inIndex, selected: this.isSelected(inIndex)}); + }, + //* Renders the list. + generateChildHtml: function() { + var h = ""; + this.index = null; + // note: can supply a rowOffset + // and indicate if rows should be rendered top down or bottomUp + for (var i=0, r=0; ienyo.Control and + enyo.Image. + + A List's _components_ block contains the controls to be used for a single + row. This set of controls will be rendered for each row. You may customize + row rendering by handling the _onSetupItem_ event. + + Events fired from within list rows contain the _index_ property, which may + be used to identify the row from which the event originated. + + The controls inside a List are non-interactive. This means that calling + methods that would normally cause rendering to occur (e.g., _setContent_) + will not do so. However, you can force a row to render by calling + _renderRow(inRow)_. + + In addition, you can force a row to be temporarily interactive by calling + _prepareRow(inRow)_. Call the _lockRow_ method when the interaction is + complete. + + For more information, see the documentation on + [Lists](https://github.com/enyojs/enyo/wiki/Lists) + in the Enyo Developer Guide. +*/ +enyo.kind({ + name: "enyo.List", + kind: "Scroller", + classes: "enyo-list", + published: { + /** + The number of rows contained in the list. Note that as the amount of + list data changes, _setRows_ can be called to adjust the number of + rows. To re-render the list at the current position when the count + has changed, call the _refresh_ method. + */ + count: 0, + /** + The number of rows to be shown on a given list page segment. + There is generally no need to adjust this value. + */ + rowsPerPage: 50, + /** + If true, renders the list such that row 0 is at the bottom of the + viewport and the beginning position of the list is scrolled to the + bottom + */ + bottomUp: false, + //* If true, multiple selections are allowed + multiSelect: false, + //* If true, the selected item will toggle + toggleSelected: false, + //* If true, the list will assume all rows have the same height for optimization + fixedHeight: false + }, + events: { + /** + Fires once per row at render time, with event object: + _{index: }_ + */ + onSetupItem: "" + }, + handlers: { + onAnimateFinish: "animateFinish" + }, + //* @protected + rowHeight: 0, + listTools: [ + {name: "port", classes: "enyo-list-port enyo-border-box", components: [ + {name: "generator", kind: "FlyweightRepeater", canGenerate: false, components: [ + {tag: null, name: "client"} + ]}, + {name: "page0", allowHtml: true, classes: "enyo-list-page"}, + {name: "page1", allowHtml: true, classes: "enyo-list-page"} + ]} + ], + create: function() { + this.pageHeights = []; + this.inherited(arguments); + this.getStrategy().translateOptimized = true; + this.bottomUpChanged(); + this.multiSelectChanged(); + this.toggleSelectedChanged(); + }, + createStrategy: function() { + this.controlParentName = "strategy"; + this.inherited(arguments); + this.createChrome(this.listTools); + this.controlParentName = "client"; + this.discoverControlParent(); + }, + rendered: function() { + this.inherited(arguments); + this.$.generator.node = this.$.port.hasNode(); + this.$.generator.generated = true; + this.reset(); + }, + resizeHandler: function() { + this.inherited(arguments); + this.refresh(); + }, + bottomUpChanged: function() { + this.$.generator.bottomUp = this.bottomUp; + this.$.page0.applyStyle(this.pageBound, null); + this.$.page1.applyStyle(this.pageBound, null); + this.pageBound = this.bottomUp ? "bottom" : "top"; + if (this.hasNode()) { + this.reset(); + } + }, + multiSelectChanged: function() { + this.$.generator.setMultiSelect(this.multiSelect); + }, + toggleSelectedChanged: function() { + this.$.generator.setToggleSelected(this.toggleSelected); + }, + countChanged: function() { + if (this.hasNode()) { + this.updateMetrics(); + } + }, + updateMetrics: function() { + this.defaultPageHeight = this.rowsPerPage * (this.rowHeight || 100); + this.pageCount = Math.ceil(this.count / this.rowsPerPage); + this.portSize = 0; + for (var i=0; i < this.pageCount; i++) { + this.portSize += this.getPageHeight(i); + } + this.adjustPortSize(); + }, + generatePage: function(inPageNo, inTarget) { + this.page = inPageNo; + var r = this.$.generator.rowOffset = this.rowsPerPage * this.page; + var rpp = this.$.generator.count = Math.min(this.count - r, this.rowsPerPage); + var html = this.$.generator.generateChildHtml(); + inTarget.setContent(html); + var pageHeight = inTarget.getBounds().height; + // if rowHeight is not set, use the height from the first generated page + if (!this.rowHeight && pageHeight > 0) { + this.rowHeight = Math.floor(pageHeight / rpp); + this.updateMetrics(); + } + // update known page heights + if (!this.fixedHeight) { + var h0 = this.getPageHeight(inPageNo); + if (h0 != pageHeight && pageHeight > 0) { + this.pageHeights[inPageNo] = pageHeight; + this.portSize += pageHeight - h0; + } + } + }, + update: function(inScrollTop) { + var updated = false; + // get page info for position + var pi = this.positionToPageInfo(inScrollTop); + // zone line position + var pos = pi.pos + this.scrollerHeight/2; + // leap-frog zone position + var k = Math.floor(pos/Math.max(pi.height, this.scrollerHeight) + 1/2) + pi.no; + // which page number for page0 (even number pages)? + var p = k % 2 == 0 ? k : k-1; + if (this.p0 != p && this.isPageInRange(p)) { + //this.log("update page0", p); + this.generatePage(p, this.$.page0); + this.positionPage(p, this.$.page0); + this.p0 = p; + updated = true; + } + // which page number for page1 (odd number pages)? + p = k % 2 == 0 ? Math.max(1, k-1) : k; + // position data page 1 + if (this.p1 != p && this.isPageInRange(p)) { + //this.log("update page1", p); + this.generatePage(p, this.$.page1); + this.positionPage(p, this.$.page1); + this.p1 = p; + updated = true; + } + if (updated && !this.fixedHeight) { + this.adjustBottomPage(); + this.adjustPortSize(); + } + }, + updateForPosition: function(inPos) { + this.update(this.calcPos(inPos)); + }, + calcPos: function(inPos) { + return (this.bottomUp ? (this.portSize - this.scrollerHeight - inPos) : inPos); + }, + adjustBottomPage: function() { + var bp = this.p0 >= this.p1 ? this.$.page0 : this.$.page1; + this.positionPage(bp.pageNo, bp); + }, + adjustPortSize: function() { + this.scrollerHeight = this.getBounds().height; + var s = Math.max(this.scrollerHeight, this.portSize); + this.$.port.applyStyle("height", s + "px"); + }, + positionPage: function(inPage, inTarget) { + inTarget.pageNo = inPage; + var y = this.pageToPosition(inPage); + inTarget.applyStyle(this.pageBound, y + "px"); + }, + pageToPosition: function(inPage) { + var y = 0; + var p = inPage; + while (p > 0) { + p--; + y += this.getPageHeight(p); + } + return y; + }, + positionToPageInfo: function(inY) { + var page = -1; + var p = this.calcPos(inY); + var h = this.defaultPageHeight; + while (p >= 0) { + page++; + h = this.getPageHeight(page); + p -= h; + } + //page = Math.min(page, this.pageCount-1); + return {no: page, height: h, pos: p+h}; + }, + isPageInRange: function(inPage) { + return inPage == Math.max(0, Math.min(this.pageCount-1, inPage)); + }, + getPageHeight: function(inPageNo) { + return this.pageHeights[inPageNo] || this.defaultPageHeight; + }, + invalidatePages: function() { + this.p0 = this.p1 = null; + // clear the html in our render targets + this.$.page0.setContent(""); + this.$.page1.setContent(""); + }, + invalidateMetrics: function() { + this.pageHeights = []; + this.rowHeight = 0; + this.updateMetrics(); + }, + scroll: function(inSender, inEvent) { + var r = this.inherited(arguments); + this.update(this.getScrollTop()); + return r; + }, + //* @public + scrollToBottom: function() { + this.update(this.getScrollBounds().maxTop); + this.inherited(arguments); + }, + setScrollTop: function(inScrollTop) { + this.update(inScrollTop); + this.inherited(arguments); + this.twiddle(); + }, + getScrollPosition: function() { + return this.calcPos(this.getScrollTop()); + }, + setScrollPosition: function(inPos) { + this.setScrollTop(this.calcPos(inPos)); + }, + //* Scrolls to a specific row. + scrollToRow: function(inRow) { + var page = Math.floor(inRow / this.rowsPerPage); + var pageRow = inRow % this.rowsPerPage; + var h = this.pageToPosition(page); + // update the page + this.updateForPosition(h); + // call pageToPosition again and this time should return the right pos since the page info is populated + h = this.pageToPosition(page); + this.setScrollPosition(h); + if (page == this.p0 || page == this.p1) { + var rowNode = this.$.generator.fetchRowNode(inRow); + if (rowNode) { + // calc row offset + var offset = rowNode.offsetTop; + if (this.bottomUp) { + offset = this.getPageHeight(page) - rowNode.offsetHeight - offset; + } + var y = this.getScrollPosition() + offset; + this.setScrollPosition(y); + } + } + }, + //* Scrolls to the beginning of the list. + scrollToStart: function() { + this[this.bottomUp ? "scrollToBottom" : "scrollToTop"](); + }, + //* Scrolls to the end of the list. + scrollToEnd: function() { + this[this.bottomUp ? "scrollToTop" : "scrollToBottom"](); + }, + //* Re-renders the list at the current position. + refresh: function() { + this.invalidatePages(); + this.update(this.getScrollTop()); + this.stabilize(); + + //FIXME: Necessary evil for Android 4.0.4 refresh bug + if (enyo.platform.android === 4) { + this.twiddle(); + } + }, + //* Re-renders the list from the beginning. + reset: function() { + this.getSelection().clear(); + this.invalidateMetrics(); + this.invalidatePages(); + this.stabilize(); + this.scrollToStart(); + }, + /** + Returns the _selection_ component that manages the selection state for + this list. + */ + getSelection: function() { + return this.$.generator.getSelection(); + }, + //* Sets the selection state for the given row index. + select: function(inIndex, inData) { + return this.getSelection().select(inIndex, inData); + }, + //* Gets the selection state for the given row index. + isSelected: function(inIndex) { + return this.$.generator.isSelected(inIndex); + }, + /** + Re-renders the specified row. Call after making modifications to a row, + to force it to render. + */ + renderRow: function(inIndex) { + this.$.generator.renderRow(inIndex); + }, + //* Prepares the row to become interactive. + prepareRow: function(inIndex) { + this.$.generator.prepareRow(inIndex); + }, + //* Restores the row to being non-interactive. + lockRow: function() { + this.$.generator.lockRow(); + }, + /** + Performs a set of tasks by running the function _inFunc_ on a row (which + must be interactive at the time the tasks are performed). Locks the row + when done. + */ + performOnRow: function(inIndex, inFunc, inContext) { + this.$.generator.performOnRow(inIndex, inFunc, inContext); + }, + //* @protected + animateFinish: function(inSender) { + this.twiddle(); + return true; + }, + // FIXME: Android 4.04 has issues with nested composited elements; for example, a SwipeableItem, + // can incorrectly generate taps on its content when it has slid off the screen; + // we address this BUG here by forcing the Scroller to "twiddle" which corrects the bug by + // provoking a dom update. + twiddle: function() { + var s = this.getStrategy(); + enyo.call(s, "twiddle"); + } +}); diff --git a/html/lib/layout/list/source/PulldownList.css b/html/lib/layout/list/source/PulldownList.css new file mode 100644 index 0000000..89f6da0 --- /dev/null +++ b/html/lib/layout/list/source/PulldownList.css @@ -0,0 +1,60 @@ +.enyo-list-pulldown { + position: absolute; + bottom: 100%; + left: 0; + right: 0; +} + +.enyo-puller { + position: relative; + height: 50px; + font-size: 22px; + color: #444; + padding: 20px 0 0px 34px; +} + +.enyo-puller-text { + position: absolute; + left: 80px; + top: 22px; +} + +.enyo-puller-arrow { + position: relative; + background: #444; + width: 7px; + height: 28px; + transition: transform 0.3s; + -webkit-transition: -webkit-transform 0.3s; + -moz-transition: -moz-transform 0.3s; + -o-transition: -o-transform 0.3s; + -ms-transition: -ms-transform 0.3s; +} + +.enyo-puller-arrow:after { + content: " "; + height: 0; + width: 0; + position: absolute; + border: 10px solid transparent; + border-bottom-color: #444; + bottom: 100%; + left: 50%; + margin-left: -10px; +} + +.enyo-puller-arrow-up { + transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -o-transform: rotate(0deg); + -ms-transform: rotate(0deg); +} + +.enyo-puller-arrow-down { + transform: rotate(180deg); + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -o-transform: rotate(180deg); + -ms-transform: rotate(180deg); +} \ No newline at end of file diff --git a/html/lib/layout/list/source/PulldownList.js b/html/lib/layout/list/source/PulldownList.js new file mode 100644 index 0000000..8d0dab1 --- /dev/null +++ b/html/lib/layout/list/source/PulldownList.js @@ -0,0 +1,180 @@ +/** +A list that provides a pull-to-refresh feature, which allows new data to be +retrieved and updated in the list. + +PulldownList provides the onPullRelease event to allow an application to start +retrieving new data. The onPullComplete event indicates that the pull is +complete and it's time to update the list with the new data. + + {name: "list", kind: "PulldownList", onSetupItem: "setupItem", + onPullRelease: "pullRelease", onPullComplete: "pullComplete", + components: [ + {name: "item"} + ]} + + pullRelease: function() { + this.search(); + }, + processSearchResults: function(inRequest, inResponse) { + this.results = inResponse.results; + this.$.list.setCount(this.results.length); + this.$.list.completePull(); + }, + pullComplete: function() { + this.$.list.reset(); + } +*/ +enyo.kind({ + name: "enyo.PulldownList", + kind: "List", + touch: true, + pully: null, + pulldownTools: [ + {name: "pulldown", classes: "enyo-list-pulldown", components: [ + {name: "puller", kind: "Puller"} + ]} + ], + events: { + onPullStart: "", + onPullCancel: "", + onPull: "", + onPullRelease: "", + onPullComplete: "" + }, + handlers: { + onScrollStart: "scrollStartHandler", + onScroll: "scrollHandler", + onScrollStop: "scrollStopHandler", + ondragfinish: "dragfinish" + }, + // + pullingMessage: "Pull down to refresh...", + pulledMessage: "Release to refresh...", + loadingMessage: "Loading...", + // + pullingIconClass: "enyo-puller-arrow enyo-puller-arrow-down", + pulledIconClass: "enyo-puller-arrow enyo-puller-arrow-up", + loadingIconClass: "", + //* @protected + create: function() { + var p = {kind: "Puller", showing: false, text: this.loadingMessage, iconClass: this.loadingIconClass, onCreate: "setPully"}; + this.listTools.splice(0, 0, p); + this.inherited(arguments); + this.setPulling(); + }, + initComponents: function() { + this.createChrome(this.pulldownTools); + this.accel = enyo.dom.canAccelerate(); + this.translation = this.accel ? "translate3d" : "translate"; + this.inherited(arguments); + }, + setPully: function(inSender, inEvent) { + this.pully = inEvent.originator; + }, + scrollStartHandler: function() { + this.firedPullStart = false; + this.firedPull = false; + this.firedPullCancel = false; + }, + scrollHandler: function(inSender) { + if (this.completingPull) { + this.pully.setShowing(false); + } + var s = this.getStrategy().$.scrollMath; + var over = s.y; + if (s.isInOverScroll() && over > 0) { + enyo.dom.transformValue(this.$.pulldown, this.translation, "0," + over + "px" + (this.accel ? ",0" : "")); + if (!this.firedPullStart) { + this.firedPullStart = true; + this.pullStart(); + this.pullHeight = this.$.pulldown.getBounds().height; + } + if (over > this.pullHeight && !this.firedPull) { + this.firedPull = true; + this.firedPullCancel = false; + this.pull(); + } + if (this.firedPull && !this.firedPullCancel && over < this.pullHeight) { + this.firedPullCancel = true; + this.firedPull = false; + this.pullCancel(); + } + } + }, + scrollStopHandler: function() { + if (this.completingPull) { + this.completingPull = false; + this.doPullComplete(); + } + }, + dragfinish: function() { + if (this.firedPull) { + var s = this.getStrategy().$.scrollMath; + s.setScrollY(s.y - this.pullHeight); + this.pullRelease(); + } + }, + //* @public + //* To signal that the list should execute pull completion. This usually be called after the application has received the new data. + completePull: function() { + this.completingPull = true; + this.$.strategy.$.scrollMath.setScrollY(this.pullHeight); + this.$.strategy.$.scrollMath.start(); + }, + //* @protected + pullStart: function() { + this.setPulling(); + this.pully.setShowing(false); + this.$.puller.setShowing(true); + this.doPullStart(); + }, + pull: function() { + this.setPulled(); + this.doPull(); + }, + pullCancel: function() { + this.setPulling(); + this.doPullCancel(); + }, + pullRelease: function() { + this.$.puller.setShowing(false); + this.pully.setShowing(true); + this.doPullRelease(); + }, + setPulling: function() { + this.$.puller.setText(this.pullingMessage); + this.$.puller.setIconClass(this.pullingIconClass); + }, + setPulled: function() { + this.$.puller.setText(this.pulledMessage); + this.$.puller.setIconClass(this.pulledIconClass); + } +}); + +enyo.kind({ + name: "enyo.Puller", + classes: "enyo-puller", + published: { + text: "", + iconClass: "" + }, + events: { + onCreate: "" + }, + components: [ + {name: "icon"}, + {name: "text", tag: "span", classes: "enyo-puller-text"} + ], + create: function() { + this.inherited(arguments); + this.doCreate(); + this.textChanged(); + this.iconClassChanged(); + }, + textChanged: function() { + this.$.text.setContent(this.text); + }, + iconClassChanged: function() { + this.$.icon.setClasses(this.iconClass); + } +}); \ No newline at end of file diff --git a/html/lib/layout/list/source/Selection.js b/html/lib/layout/list/source/Selection.js new file mode 100644 index 0000000..8581f8d --- /dev/null +++ b/html/lib/layout/list/source/Selection.js @@ -0,0 +1,141 @@ +/** + _enyo.Selection_ is used to manage row selection state for lists. It + provides selection state management for both single-select and multi-select + lists. + + // The following is an excerpt from enyo.FlyweightRepeater. + enyo.kind({ + name: "enyo.FlyweightRepeater", + ... + components: [ + {kind: "Selection", onSelect: "selectDeselect", onDeselect: "selectDeselect"}, + ... + ], + tap: function(inSender, inEvent) { + ... + // mark the tapped row as selected + this.$.selection.select(inEvent.index); + ... + }, + selectDeselect: function(inSender, inEvent) { + // this is where a row selection highlight might be applied + this.renderRow(inEvent.key); + } + ... + }) +*/ +enyo.kind({ + name: "enyo.Selection", + kind: enyo.Component, + published: { + //* If true, multiple selections are allowed. + multi: false + }, + events: { + /** + Fires when an item is selected. + + {kind: "Selection", onSelect: "selectRow"... + ... + selectRow: function(inSender, inKey, inPrivateData) { + ... + + _inKey_ is whatever key was used to register + the selection (usually a row index). + + _inPrivateData_ references data registered + with this key by the code that made the original selection. + */ + onSelect: "", + /** + Fires when an item is deselected. + + {kind: "Selection", onSelect: "deselectRow"... + ... + deselectRow: function(inSender, inKey, inPrivateData) + ... + + _inKey_ is whatever key was used to request + the deselection (usually a row index). + + _inPrivateData_ references data registered + with this key by the code that made the selection. + */ + onDeselect: "", + //* Sent when selection changes (but not when the selection is cleared). + onChange: "" + }, + //* @protected + create: function() { + this.clear(); + this.inherited(arguments); + }, + multiChanged: function() { + if (!this.multi) { + this.clear(); + } + this.doChange(); + }, + highlander: function(inKey) { + if (!this.multi) { + this.deselect(this.lastSelected); + } + }, + //* @public + //* Removes all selections. + clear: function() { + this.selected = {}; + }, + //* Returns true if the _inKey_ row is selected. + isSelected: function(inKey) { + return this.selected[inKey]; + }, + //* Manually sets a row's state to selected or unselected. + setByKey: function(inKey, inSelected, inData) { + if (inSelected) { + this.selected[inKey] = (inData || true); + this.lastSelected = inKey; + this.doSelect({key: inKey, data: this.selected[inKey]}); + } else { + var was = this.isSelected(inKey); + delete this.selected[inKey]; + this.doDeselect({key: inKey, data: was}); + } + this.doChange(); + }, + //* Deselects a row. + deselect: function(inKey) { + if (this.isSelected(inKey)) { + this.setByKey(inKey, false); + } + }, + /** + Selects a row. If the _multi_ property is set to false, _select_ will + also deselect the previous selection. + */ + select: function(inKey, inData) { + if (this.multi) { + this.setByKey(inKey, !this.isSelected(inKey), inData); + } else if (!this.isSelected(inKey)) { + this.highlander(); + this.setByKey(inKey, true, inData); + } + }, + /** + Toggles selection state for a row. If the _multi_ property is set to + false, toggling a selection on will deselect the previous selection. + */ + toggle: function(inKey, inData) { + if (!this.multi && this.lastSelected != inKey) { + this.deselect(this.lastSelected); + } + this.setByKey(inKey, !this.isSelected(inKey), inData); + }, + /** + Returns the selection as a hash in which each selected item has a value; + unselected items are undefined. + */ + getSelected: function() { + return this.selected; + } +}); diff --git a/html/lib/layout/list/source/package.js b/html/lib/layout/list/source/package.js new file mode 100644 index 0000000..391e38e --- /dev/null +++ b/html/lib/layout/list/source/package.js @@ -0,0 +1,7 @@ +enyo.depends( + "FlyweightRepeater.js", + "List.css", + "List.js", + "PulldownList.css", + "PulldownList.js" +); \ No newline at end of file diff --git a/html/lib/layout/list/source/wip-package.js b/html/lib/layout/list/source/wip-package.js new file mode 100644 index 0000000..bafc093 --- /dev/null +++ b/html/lib/layout/list/source/wip-package.js @@ -0,0 +1,6 @@ +enyo.depends( + // Add wip controls here + "AlphaJumper.css", + "AlphaJumper.js", + "AlphaJumpList.js" +); \ No newline at end of file diff --git a/html/lib/layout/package.js b/html/lib/layout/package.js new file mode 100644 index 0000000..7f9777f --- /dev/null +++ b/html/lib/layout/package.js @@ -0,0 +1,7 @@ +enyo.depends( + "fittable", + "list", + "slideable", + "panels", + "tree" +); \ No newline at end of file diff --git a/html/lib/layout/panels/package.js b/html/lib/layout/panels/package.js new file mode 100644 index 0000000..f1de4f2 --- /dev/null +++ b/html/lib/layout/panels/package.js @@ -0,0 +1,3 @@ +enyo.depends( + "source/" +); \ No newline at end of file diff --git a/html/lib/layout/panels/source/Panels.css b/html/lib/layout/panels/source/Panels.css new file mode 100644 index 0000000..2e79349 --- /dev/null +++ b/html/lib/layout/panels/source/Panels.css @@ -0,0 +1,12 @@ +.enyo-panels { +} + +.enyo-panels-fit-narrow { +} + +@media all and (max-width: 800px) { + .enyo-panels-fit-narrow > * { + min-width: 100%; + max-width: 100%; + } +} \ No newline at end of file diff --git a/html/lib/layout/panels/source/Panels.js b/html/lib/layout/panels/source/Panels.js new file mode 100644 index 0000000..d2e2ecc --- /dev/null +++ b/html/lib/layout/panels/source/Panels.js @@ -0,0 +1,386 @@ +/** +The enyo.Panels kind is designed to satisfy a variety of common use cases for +application layout. Using enyo.Panels, controls may be arranged as (among other +things) a carousel, a set of collapsing panels, a card stack that fades between +panels, or a grid. + +Any Enyo control may be placed inside an enyo.Panels, but by convention we refer +to each of these controls as a "panel." From the set of panels in an enyo.Panels, +one is considered active. The active panel is set by index using the *setIndex* +method. The actual layout of the panels typically changes each time the active +panel is set, such that the new active panel has the most prominent position. + +For more information, see the [Panels documentation](https://github.com/enyojs/enyo/wiki/Panels) +in the Enyo Developer Guide. +*/ +enyo.kind({ + name: "enyo.Panels", + classes: "enyo-panels", + published: { + /** + The index of the active panel. The layout of panels is controlled by + the layoutKind, but as a rule, the active panel is displayed in the + most prominent position. For example, in the (default) CardArranger + layout, the active panel is shown and the other panels are hidden. + */ + index: 0, + //* Controls whether the user can drag between panels. + draggable: true, + //* Controls whether the panels animate when transitioning; for example, + //* when _setIndex_ is called. + animate: true, + //* Controls whether panels "wrap around" when moving past the end. Actual effect depends upon the arranger in use. + wrap: false, + //* Sets the arranger kind to be used for dynamic layout. + arrangerKind: "CardArranger", + //* By default, each panel will be sized to fit the Panels' width when + //* the screen size is narrow enough (less than ~800px). Set to false + //* to avoid this behavior. + narrowFit: true + }, + events: { + /** + Fires at the start of a panel transition. + This event fires when _setIndex_ is called and also during dragging. + */ + onTransitionStart: "", + /** + Fires at the end of a panel transition. + This event fires when _setIndex_ is called and also during dragging. + */ + onTransitionFinish: "" + }, + //* @protected + handlers: { + ondragstart: "dragstart", + ondrag: "drag", + ondragfinish: "dragfinish" + }, + tools: [ + {kind: "Animator", onStep: "step", onEnd: "completed"} + ], + fraction: 0, + create: function() { + this.transitionPoints = []; + this.inherited(arguments); + this.arrangerKindChanged(); + this.avoidFitChanged(); + this.indexChanged(); + }, + initComponents: function() { + this.createChrome(this.tools); + this.inherited(arguments); + }, + arrangerKindChanged: function() { + this.setLayoutKind(this.arrangerKind); + }, + avoidFitChanged: function() { + this.addRemoveClass("enyo-panels-fit-narrow", this.narrowFit); + }, + removeControl: function(inControl) { + this.inherited(arguments); + if (this.controls.length > 1 && this.isPanel(inControl)) { + this.setIndex(Math.max(this.index - 1, 0)); + this.flow(); + this.reflow(); + } + }, + isPanel: function() { + // designed to be overridden in kinds derived from Panels that have + // non-panel client controls + return true; + }, + flow: function() { + this.arrangements = []; + this.inherited(arguments); + }, + reflow: function() { + this.arrangements = []; + this.inherited(arguments); + this.refresh(); + }, + //* @public + /** + Returns an array of contained panels. + Subclasses can override this if they don't want the arranger to layout all of their children + */ + getPanels: function() { + var p = this.controlParent || this; + return p.children; + }, + //* Returns a reference to the active panel--i.e., the panel at the specified index. + getActive: function() { + var p$ = this.getPanels(); + return p$[this.index]; + }, + /** + Returns a reference to the enyo.Animator + instance used to animate panel transitions. The Panels' animator can be used + to set the duration of panel transitions, e.g.: + + this.getAnimator().setDuration(1000); + */ + getAnimator: function() { + return this.$.animator; + }, + /** + Sets the active panel to the panel specified by the given index. + Note that if the _animate_ property is set to true, the active panel + will animate into view. + */ + setIndex: function(inIndex) { + // override setIndex so that indexChanged is called + // whether this.index has actually changed or not + this.setPropertyValue("index", inIndex, "indexChanged"); + }, + /** + Sets the active panel to the panel specified by the given index. + Regardless of the value of the _animate_ property, the transition to the + next panel will not animate and will be immediate. + */ + setIndexDirect: function(inIndex) { + this.setIndex(inIndex); + this.completed(); + }, + //* Transitions to the previous panel--i.e., the panel whose index value is + //* one less than that of the current active panel. + previous: function() { + this.setIndex(this.index-1); + }, + //* Transitions to the next panel--i.e., the panel whose index value is one + //* greater than that of the current active panel. + next: function() { + this.setIndex(this.index+1); + }, + //* @protected + clamp: function(inValue) { + var l = this.getPanels().length-1; + if (this.wrap) { + // FIXME: dragging makes assumptions about direction and from->start indexes. + //return inValue < 0 ? l : (inValue > l ? 0 : inValue); + return inValue; + } else { + return Math.max(0, Math.min(inValue, l)); + } + }, + indexChanged: function(inOld) { + this.lastIndex = inOld; + this.index = this.clamp(this.index); + if (!this.dragging) { + if (this.$.animator.isAnimating()) { + this.completed(); + } + this.$.animator.stop(); + if (this.hasNode()) { + if (this.animate) { + this.startTransition(); + this.$.animator.play({ + startValue: this.fraction + }); + } else { + this.refresh(); + } + } + } + }, + step: function(inSender) { + this.fraction = inSender.value; + this.stepTransition(); + }, + completed: function() { + if (this.$.animator.isAnimating()) { + this.$.animator.stop(); + } + this.fraction = 1; + this.stepTransition(); + this.finishTransition(); + }, + dragstart: function(inSender, inEvent) { + if (this.draggable && this.layout && this.layout.canDragEvent(inEvent)) { + inEvent.preventDefault(); + this.dragstartTransition(inEvent); + this.dragging = true; + this.$.animator.stop(); + return true; + } + }, + drag: function(inSender, inEvent) { + if (this.dragging) { + inEvent.preventDefault(); + this.dragTransition(inEvent); + } + }, + dragfinish: function(inSender, inEvent) { + if (this.dragging) { + this.dragging = false; + inEvent.preventTap(); + this.dragfinishTransition(inEvent); + } + }, + dragstartTransition: function(inEvent) { + if (!this.$.animator.isAnimating()) { + var f = this.fromIndex = this.index; + this.toIndex = f - (this.layout ? this.layout.calcDragDirection(inEvent) : 0); + } else { + this.verifyDragTransition(inEvent); + } + this.fromIndex = this.clamp(this.fromIndex); + this.toIndex = this.clamp(this.toIndex); + //this.log(this.fromIndex, this.toIndex); + this.fireTransitionStart(); + if (this.layout) { + this.layout.start(); + } + }, + dragTransition: function(inEvent) { + // note: for simplicity we choose to calculate the distance directly between + // the first and last transition point. + var d = this.layout ? this.layout.calcDrag(inEvent) : 0; + var t$ = this.transitionPoints, s = t$[0], f = t$[t$.length-1]; + var as = this.fetchArrangement(s); + var af = this.fetchArrangement(f); + var dx = this.layout ? this.layout.drag(d, s, as, f, af) : 0; + var dragFail = d && !dx; + if (dragFail) { + //this.log(dx, s, as, f, af); + } + this.fraction += dx; + var fr = this.fraction; + if (fr > 1 || fr < 0 || dragFail) { + if (fr > 0 || dragFail) { + this.dragfinishTransition(inEvent); + } + this.dragstartTransition(inEvent); + this.fraction = 0; + // FIXME: account for lost fraction + //this.dragTransition(inEvent); + } + this.stepTransition(); + }, + dragfinishTransition: function(inEvent) { + this.verifyDragTransition(inEvent); + this.setIndex(this.toIndex); + // note: if we're still dragging, then we're at a transition boundary + // and should fire the finish event + if (this.dragging) { + this.fireTransitionFinish(); + } + }, + verifyDragTransition: function(inEvent) { + var d = this.layout ? this.layout.calcDragDirection(inEvent) : 0; + var f = Math.min(this.fromIndex, this.toIndex); + var t = Math.max(this.fromIndex, this.toIndex); + if (d > 0) { + var s = f; + f = t; + t = s; + } + if (f != this.fromIndex) { + this.fraction = 1 - this.fraction; + } + //this.log("old", this.fromIndex, this.toIndex, "new", f, t); + this.fromIndex = f; + this.toIndex = t; + }, + refresh: function() { + if (this.$.animator.isAnimating()) { + this.$.animator.stop(); + } + this.startTransition(); + this.fraction = 1; + this.stepTransition(); + this.finishTransition(); + }, + startTransition: function() { + this.fromIndex = this.fromIndex != null ? this.fromIndex : this.lastIndex || 0; + this.toIndex = this.toIndex != null ? this.toIndex : this.index; + //this.log(this.id, this.fromIndex, this.toIndex); + if (this.layout) { + this.layout.start(); + } + this.fireTransitionStart(); + }, + finishTransition: function() { + if (this.layout) { + this.layout.finish(); + } + this.transitionPoints = []; + this.fraction = 0; + this.fromIndex = this.toIndex = null; + this.fireTransitionFinish(); + }, + fireTransitionStart: function() { + var t = this.startTransitionInfo; + if (this.hasNode() && (!t || (t.fromIndex != this.fromIndex || t.toIndex != this.toIndex))) { + this.startTransitionInfo = {fromIndex: this.fromIndex, toIndex: this.toIndex}; + this.doTransitionStart(enyo.clone(this.startTransitionInfo)); + } + }, + fireTransitionFinish: function() { + var t = this.finishTransitionInfo; + if (this.hasNode() && (!t || (t.fromIndex != this.lastIndex || t.toIndex != this.index))) { + this.finishTransitionInfo = {fromIndex: this.lastIndex, toIndex: this.index}; + this.doTransitionFinish(enyo.clone(this.finishTransitionInfo)); + } + this.lastIndex=this.index; + }, + // gambit: we interpolate between arrangements as needed. + stepTransition: function() { + if (this.hasNode()) { + // select correct transition points and normalize fraction. + var t$ = this.transitionPoints; + var r = (this.fraction || 0) * (t$.length-1); + var i = Math.floor(r); + r = r - i; + var s = t$[i], f = t$[i+1]; + // get arrangements and lerp between them + var s0 = this.fetchArrangement(s); + var s1 = this.fetchArrangement(f); + this.arrangement = s0 && s1 ? enyo.Panels.lerp(s0, s1, r) : (s0 || s1); + if (this.arrangement && this.layout) { + this.layout.flowArrangement(); + } + } + }, + fetchArrangement: function(inName) { + if ((inName != null) && !this.arrangements[inName] && this.layout) { + this.layout._arrange(inName); + this.arrangements[inName] = this.readArrangement(this.getPanels()); + } + return this.arrangements[inName]; + }, + readArrangement: function(inC) { + var r = []; + for (var i=0, c$=inC, c; (c=c$[i]); i++) { + r.push(enyo.clone(c._arranger)); + } + return r; + }, + statics: { + isScreenNarrow: function() { + return enyo.dom.getWindowWidth() <= 800; + }, + lerp: function(inA0, inA1, inFrac) { + var r = []; + for (var i=0, k$=enyo.keys(inA0), k; (k=k$[i]); i++) { + r.push(this.lerpObject(inA0[k], inA1[k], inFrac)); + } + return r; + }, + lerpObject: function(inNew, inOld, inFrac) { + var b = enyo.clone(inNew), n, o; + // inOld might be undefined when deleting panels + if (inOld) { + for (var i in inNew) { + n = inNew[i]; + o = inOld[i]; + if (n != o) { + b[i] = n - (n - o) * inFrac; + } + } + } + return b; + } + } +}); + diff --git a/html/lib/layout/panels/source/arrangers/Arranger.css b/html/lib/layout/panels/source/arrangers/Arranger.css new file mode 100644 index 0000000..4d23be0 --- /dev/null +++ b/html/lib/layout/panels/source/arrangers/Arranger.css @@ -0,0 +1,29 @@ +.enyo-arranger { + position: relative; + overflow: hidden; +} + +.enyo-arranger.enyo-fit { + position: absolute; +} + +.enyo-arranger > * { + position: absolute; + left: 0; + top: 0; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; +} + +.enyo-arranger-fit > * { + /* + override any width/height set on panels + */ + width: 100% !important; + height: 100% !important; + min-width: 0 !important; + max-width: auto !important; + min-height: 0 !important; + max-height: auto !important; +} diff --git a/html/lib/layout/panels/source/arrangers/Arranger.js b/html/lib/layout/panels/source/arrangers/Arranger.js new file mode 100644 index 0000000..b10f6b8 --- /dev/null +++ b/html/lib/layout/panels/source/arrangers/Arranger.js @@ -0,0 +1,226 @@ +/** + _enyo.Arranger_ is an enyo.Layout that considers + one of the controls it lays out as active. The other controls are placed + relative to the active control as makes sense for the layout. + + Arranger supports dynamic layouts, meaning it's possible to transition + between its layouts via animation. Typically, arrangers should lay out + controls using CSS transforms, since these are optimized for animation. To + support this, the controls in an Arranger are absolutely positioned, and + the Arranger kind has an `accelerated` property, which marks controls for + CSS compositing. The default setting of "auto" ensures that this will occur + if enabled by the platform. + + For more information, see the documentation on + [Arrangers](https://github.com/enyojs/enyo/wiki/Arrangers) + in the Enyo Developer Guide. +*/ +enyo.kind({ + name: "enyo.Arranger", + kind: "Layout", + layoutClass: "enyo-arranger", + /** + Sets controls being laid out to use CSS compositing. A setting of "auto" + will mark controls for compositing if the platform supports it. + */ + accelerated: "auto", + //* Property of the drag event used to calculate the amount a drag moves the layout + dragProp: "ddx", + //* Property of the drag event used to calculate the direction of a drag + dragDirectionProp: "xDirection", + //* Property of the drag event used to calculate whether a drag should occur + canDragProp: "horizontal", + /** + If set to true, transitions between non-adjacent arrangements will go + through the intermediate arrangements. This is useful when direct + transitions between arrangements would be visually jarring. + */ + incrementalPoints: false, + /** + Called when removing an arranger (for example, when switching a Panels + control to a different arrangerKind). Subclasses should implement this + function to reset whatever properties they've changed on child controls. + You *must* call the superclass implementation in your subclass's + _destroy_ function. + */ + destroy: function() { + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + c._arranger = null; + } + this.inherited(arguments); + }, + //* @public + /** + Arranges the given array of controls (_inC_) in the layout specified by + _inName_. When implementing this method, rather than apply styling + directly to controls, call _arrangeControl(inControl, inArrangement)_ + with an _inArrangement_ object with styling settings. These will then be + applied via the _flowControl(inControl, inArrangement)_ method. + */ + arrange: function(inC, inName) { + }, + /** + Sizes the controls in the layout. This method is called only at reflow + time. Note that sizing is separated from other layout done in the + _arrange_ method because it is expensive and not suitable for dynamic + layout. + */ + size: function() { + }, + /** + Called when a layout transition begins. Implement this method to perform + tasks that should only occur when a transition starts; for example, some + controls could be shown or hidden. In addition, the _transitionPoints_ + array may be set on the container to dictate the named arrangments + between which the transition occurs. + */ + start: function() { + var f = this.container.fromIndex, t = this.container.toIndex; + var p$ = this.container.transitionPoints = [f]; + // optionally add a transition point for each index between from and to. + if (this.incrementalPoints) { + var d = Math.abs(t - f) - 2; + var i = f; + while (d >= 0) { + i = i + (t < f ? -1 : 1); + p$.push(i); + d--; + } + } + p$.push(this.container.toIndex); + }, + /** + Called when a layout transition completes. Implement this method to + perform tasks that should only occur when a transition ends; for + example, some controls could be shown or hidden. + */ + finish: function() { + }, + //* @protected + canDragEvent: function(inEvent) { + return inEvent[this.canDragProp]; + }, + calcDragDirection: function(inEvent) { + return inEvent[this.dragDirectionProp]; + }, + calcDrag: function(inEvent) { + return inEvent[this.dragProp]; + }, + drag: function(inDp, inAn, inA, inBn, inB) { + var f = this.measureArrangementDelta(-inDp, inAn, inA, inBn, inB); + return f; + }, + measureArrangementDelta: function(inX, inI0, inA0, inI1, inA1) { + var d = this.calcArrangementDifference(inI0, inA0, inI1, inA1); + var s = d ? inX / Math.abs(d) : 0; + s = s * (this.container.fromIndex > this.container.toIndex ? -1 : 1); + //enyo.log("delta", s); + return s; + }, + //* @public + /** + Called when dragging the layout, this method returns the difference in + pixels between the arrangement _inA0_ for layout setting _inI0_ and + arrangement _inA1_ for layout setting _inI1_. This data is used to calculate + the percentage that a drag should move the layout between two active states. + */ + calcArrangementDifference: function(inI0, inA0, inI1, inA1) { + }, + //* @protected + _arrange: function(inIndex) { + var c$ = this.getOrderedControls(inIndex); + this.arrange(c$, inIndex); + }, + arrangeControl: function(inControl, inArrangement) { + inControl._arranger = enyo.mixin(inControl._arranger || {}, inArrangement); + }, + flow: function() { + this.c$ = [].concat(this.container.getPanels()); + this.controlsIndex = 0; + for (var i=0, c$=this.container.getPanels(), c; c=c$[i]; i++) { + enyo.dom.accelerate(c, this.accelerated); + if (enyo.platform.safari) { + // On Safari-desktop, sometimes having the panel's direct child set to accelerate isn't sufficient + // this is most often the case with Lists contained inside another control, inside a Panels + var grands=c.children; + for (var j=0, kid; kid=grands[j]; j++) { + enyo.dom.accelerate(kid, this.accelerated); + } + } + } + }, + reflow: function() { + var cn = this.container.hasNode(); + this.containerBounds = cn ? {width: cn.clientWidth, height: cn.clientHeight} : {}; + this.size(); + }, + flowArrangement: function() { + var a = this.container.arrangement; + if (a) { + for (var i=0, c$=this.container.getPanels(), c; c=c$[i]; i++) { + this.flowControl(c, a[i]); + } + } + }, + //* @public + /** + Lays out the control (_inControl_) according to the settings stored in + the _inArrangment_ object. By default, _flowControl_ will apply settings + of left, top, and opacity. This method should only be implemented to + apply other settings made via _arrangeControl_. + */ + flowControl: function(inControl, inArrangement) { + enyo.Arranger.positionControl(inControl, inArrangement); + var o = inArrangement.opacity; + if (o != null) { + enyo.Arranger.opacifyControl(inControl, o); + } + }, + //* @protected + // Gets an array of controls arranged in state order. + // note: optimization, dial around a single array. + getOrderedControls: function(inIndex) { + var whole = Math.floor(inIndex); + var a = whole - this.controlsIndex; + var sign = a > 0; + var c$ = this.c$ || []; + for (var i=0; i .99 ? 1 : (o < .01 ? 0 : o); + // note: we only care about ie8 + if (enyo.platform.ie < 9) { + inControl.applyStyle("filter", "progid:DXImageTransform.Microsoft.Alpha(Opacity=" + (o * 100) + ")"); + } else { + inControl.applyStyle("opacity", o); + } + } + } +}); diff --git a/html/lib/layout/panels/source/arrangers/CardArranger.js b/html/lib/layout/panels/source/arrangers/CardArranger.js new file mode 100644 index 0000000..28b94b2 --- /dev/null +++ b/html/lib/layout/panels/source/arrangers/CardArranger.js @@ -0,0 +1,49 @@ +/** + _enyo.CardArranger_ is an enyo.Arranger that + displays only one active control. The non-active controls are hidden with + _setShowing(false)_. Transitions between arrangements are handled by fading + from one control to the next. +*/ +enyo.kind({ + name: "enyo.CardArranger", + kind: "Arranger", + layoutClass: "enyo-arranger enyo-arranger-fit", + calcArrangementDifference: function(inI0, inA0, inI1, inA1) { + return this.containerBounds.width; + }, + arrange: function(inC, inName) { + for (var i=0, c, b, v; c=inC[i]; i++) { + v = (i == 0) ? 1 : 0; + this.arrangeControl(c, {opacity: v}); + } + }, + start: function() { + this.inherited(arguments); + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + var wasShowing=c.showing; + c.setShowing(i == this.container.fromIndex || i == (this.container.toIndex)); + if (c.showing && !wasShowing) { + c.resized(); + } + } + + }, + finish: function() { + this.inherited(arguments); + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + c.setShowing(i == this.container.toIndex); + } + }, + destroy: function() { + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + enyo.Arranger.opacifyControl(c, 1); + if (!c.showing) { + c.setShowing(true); + } + } + this.inherited(arguments); + } +}); diff --git a/html/lib/layout/panels/source/arrangers/CardSlideInArranger.js b/html/lib/layout/panels/source/arrangers/CardSlideInArranger.js new file mode 100644 index 0000000..7700557 --- /dev/null +++ b/html/lib/layout/panels/source/arrangers/CardSlideInArranger.js @@ -0,0 +1,62 @@ +/** + _enyo.CardSlideInArranger_ is an enyo.Arranger + that displays only one active control. The non-active controls are hidden + with _setShowing(false)_. Transitions between arrangements are handled by + sliding the new control over the current one. + + Note that CardSlideInArranger always slides controls in from the right. If + you want an arranger that slides to the right and left, try + enyo.LeftRightArranger. +*/ +enyo.kind({ + name: "enyo.CardSlideInArranger", + kind: "CardArranger", + start: function() { + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + var wasShowing=c.showing; + c.setShowing(i == this.container.fromIndex || i == (this.container.toIndex)); + if (c.showing && !wasShowing) { + c.resized(); + } + } + var l = this.container.fromIndex; + var i = this.container.toIndex; + this.container.transitionPoints = [ + i + "." + l + ".s", + i + "." + l + ".f" + ]; + }, + finish: function() { + this.inherited(arguments); + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + c.setShowing(i == this.container.toIndex); + } + }, + arrange: function(inC, inName) { + var p = inName.split("."); + var f = p[0], s= p[1], starting = (p[2] == "s"); + var b = this.containerBounds.width; + for (var i=0, c$=this.container.getPanels(), c, v; c=c$[i]; i++) { + v = b; + if (s == i) { + v = starting ? 0 : -b; + } + if (f == i) { + v = starting ? b : 0; + } + if (s == i && s == f) { + v = 0; + } + this.arrangeControl(c, {left: v}); + } + }, + destroy: function() { + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + enyo.Arranger.positionControl(c, {left: null}); + } + this.inherited(arguments); + } +}); diff --git a/html/lib/layout/panels/source/arrangers/CarouselArranger.js b/html/lib/layout/panels/source/arrangers/CarouselArranger.js new file mode 100644 index 0000000..3a2f26a --- /dev/null +++ b/html/lib/layout/panels/source/arrangers/CarouselArranger.js @@ -0,0 +1,109 @@ +/** + _enyo.CarouselArranger_ is an enyo.Arranger + that displays the active control, along with some number of inactive + controls to fill the available space. The active control is positioned on + the left side of the container, and the rest of the views are laid out to + the right. + + One of the controls may have _fit: true_ set, in which case it will take up + any remaining space after all of the other controls have been sized. + + For best results with CarouselArranger, you should set a minimum width for + each control via a CSS style, e.g., _min-width: 25%_ or _min-width: 250px_. + + Transitions between arrangements are handled by sliding the new controls in + from the right and sliding the old controls off to the left. +*/ +enyo.kind({ + name: "enyo.CarouselArranger", + kind: "Arranger", + size: function() { + var c$ = this.container.getPanels(); + var padding = this.containerPadding = this.container.hasNode() ? enyo.FittableLayout.calcPaddingExtents(this.container.node) : {}; + var pb = this.containerBounds; + pb.height -= padding.top + padding.bottom; + pb.width -= padding.left + padding.right; + // used space + var fit; + for (var i=0, s=0, m, c; c=c$[i]; i++) { + m = enyo.FittableLayout.calcMarginExtents(c.hasNode()); + c.width = c.getBounds().width; + c.marginWidth = m.right + m.left; + s += (c.fit ? 0 : c.width) + c.marginWidth; + if (c.fit) { + fit = c; + } + } + if (fit) { + var w = pb.width - s; + fit.width = w >= 0 ? w : fit.width; + } + for (var i=0, e=padding.left, m, c; c=c$[i]; i++) { + c.setBounds({top: padding.top, bottom: padding.bottom, width: c.fit ? c.width : null}); + } + }, + arrange: function(inC, inName) { + if (this.container.wrap) { + this.arrangeWrap(inC, inName); + } else { + this.arrangeNoWrap(inC, inName); + } + }, + arrangeNoWrap: function(inC, inName) { + var c$ = this.container.getPanels(); + var s = this.container.clamp(inName); + var nw = this.containerBounds.width; + // do we have enough content to fill the width? + for (var i=s, cw=0, c; c=c$[i]; i++) { + cw += c.width + c.marginWidth; + if (cw > nw) { + break; + } + } + // if content width is less than needed, adjust starting point index and offset + var n = nw - cw; + var o = 0; + if (n > 0) { + var s1 = s; + for (var i=s-1, aw=0, c; c=c$[i]; i--) { + aw += c.width + c.marginWidth; + if (n - aw <= 0) { + o = (n - aw); + s = i; + break; + } + } + } + // arrange starting from needed index with detected offset so we fill space + for (var i=0, e=this.containerPadding.left + o, w, c; c=c$[i]; i++) { + w = c.width + c.marginWidth; + if (i < s) { + this.arrangeControl(c, {left: -w}); + } else { + this.arrangeControl(c, {left: Math.floor(e)}); + e += w; + } + } + }, + arrangeWrap: function(inC, inName) { + for (var i=0, e=this.containerPadding.left, m, c; c=inC[i]; i++) { + this.arrangeControl(c, {left: e}); + e += c.width + c.marginWidth; + } + }, + calcArrangementDifference: function(inI0, inA0, inI1, inA1) { + var i = Math.abs(inI0 % this.c$.length); + return inA0[i].left - inA1[i].left; + }, + destroy: function() { + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + enyo.Arranger.positionControl(c, {left: null, top: null}); + c.applyStyle("top", null); + c.applyStyle("bottom", null); + c.applyStyle("left", null); + c.applyStyle("width", null); + } + this.inherited(arguments); + } +}); diff --git a/html/lib/layout/panels/source/arrangers/CollapsingArranger.js b/html/lib/layout/panels/source/arrangers/CollapsingArranger.js new file mode 100644 index 0000000..799c94b --- /dev/null +++ b/html/lib/layout/panels/source/arrangers/CollapsingArranger.js @@ -0,0 +1,80 @@ +/** + _enyo.CollapsingArranger_ is an enyo.Arranger + that displays the active control, along with some number of inactive + controls to fill the available space. The active control is positioned on + the left side of the container and the rest of the views are laid out to the + right. The last control, if visible, will expand to fill whatever space is + not taken up by the previous controls. + + For best results with CollapsingArranger, you should set a minimum width + for each control via a CSS style, e.g., _min-width: 25%_ or + _min-width: 250px_. + + Transitions between arrangements are handled by sliding the new control in + from the right and collapsing the old control to the left. +*/ +enyo.kind({ + name: "enyo.CollapsingArranger", + kind: "CarouselArranger", + size: function() { + this.clearLastSize(); + this.inherited(arguments); + }, + //* @protected + // clear size from last if it's not actually the last + // (required for adding another control) + clearLastSize: function() { + for (var i=0, c$=this.container.getPanels(), c; c=c$[i]; i++) { + if (c._fit && i != c$.length-1) { + c.applyStyle("width", null); + c._fit = null; + } + } + }, + //* @public + arrange: function(inC, inIndex) { + var c$ = this.container.getPanels(); + for (var i=0, e=this.containerPadding.left, m, c; c=c$[i]; i++) { + this.arrangeControl(c, {left: e}); + if (i >= inIndex) { + e += c.width + c.marginWidth; + } + // FIXME: overdragging-ish + if (i == c$.length - 1 && inIndex < 0) { + this.arrangeControl(c, {left: e - inIndex}); + } + } + }, + calcArrangementDifference: function(inI0, inA0, inI1, inA1) { + var i = this.container.getPanels().length-1; + return Math.abs(inA1[i].left - inA0[i].left); + }, + flowControl: function(inControl, inA) { + this.inherited(arguments); + if (this.container.realtimeFit) { + var c$ = this.container.getPanels(); + var l = c$.length-1; + var last = c$[l]; + if (inControl == last) { + this.fitControl(inControl, inA.left); + } + } + + }, + finish: function() { + this.inherited(arguments); + if (!this.container.realtimeFit && this.containerBounds) { + var c$ = this.container.getPanels(); + var a$ = this.container.arrangement; + var l = c$.length-1; + var c = c$[l]; + this.fitControl(c, a$[l].left); + } + }, + //* @protected + fitControl: function(inControl, inOffset) { + inControl._fit = true; + inControl.applyStyle("width", (this.containerBounds.width - inOffset) + "px"); + inControl.resized(); + } +}); diff --git a/html/lib/layout/panels/source/arrangers/OtherArrangers.js b/html/lib/layout/panels/source/arrangers/OtherArrangers.js new file mode 100644 index 0000000..533bac3 --- /dev/null +++ b/html/lib/layout/panels/source/arrangers/OtherArrangers.js @@ -0,0 +1,202 @@ +/** + _enyo.LeftRightArranger_ is an enyo.Arranger + that displays the active control and some of the previous and next controls. + The active control is centered horizontally in the container, and the + previous and next controls are laid out to the left and right, respectively. + + Transitions between arrangements are handled by sliding the new control + in from the right and sliding the active control out to the left. +*/ +enyo.kind({ + name: "enyo.LeftRightArranger", + kind: "Arranger", + //* The margin width (i.e., how much of the previous and next controls + //* are visible) in pixels + margin: 40, + //* @protected + axisSize: "width", + offAxisSize: "height", + axisPosition: "left", + constructor: function() { + this.inherited(arguments); + this.margin = this.container.margin != null ? this.container.margin : this.margin; + }, + //* @public + size: function() { + var c$ = this.container.getPanels(); + var port = this.containerBounds[this.axisSize]; + var box = port - this.margin -this.margin; + for (var i=0, b, c; c=c$[i]; i++) { + b = {}; + b[this.axisSize] = box; + b[this.offAxisSize] = "100%"; + c.setBounds(b); + } + }, + arrange: function(inC, inIndex) { + var o = Math.floor(this.container.getPanels().length/2); + var c$ = this.getOrderedControls(Math.floor(inIndex)-o); + var box = this.containerBounds[this.axisSize] - this.margin -this.margin; + var e = this.margin - box * o; + var m = (c$.length - 1) / 2; + for (var i=0, c, b, v; c=c$[i]; i++) { + b = {}; + b[this.axisPosition] = e; + b.opacity = (i == 0 || i == c$.length-1) ? 0 : 1; + this.arrangeControl(c, b); + e += box; + } + }, + calcArrangementDifference: function(inI0, inA0, inI1, inA1) { + var i = Math.abs(inI0 % this.c$.length); + //enyo.log(inI0, inI1); + return inA0[i][this.axisPosition] - inA1[i][this.axisPosition]; + }, + destroy: function() { + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + enyo.Arranger.positionControl(c, {left: null, top: null}); + enyo.Arranger.opacifyControl(c, 1); + c.applyStyle("left", null); + c.applyStyle("top", null); + c.applyStyle("height", null); + c.applyStyle("width", null); + } + this.inherited(arguments); + } +}); + +/** + _enyo.TopBottomArranger_ is an enyo.Arranger + that displays the active control and some of the previous and next controls. + The active control is centered vertically in the container, and the previous + and next controls are laid out above and below, respectively. + + Transitions between arrangements are handled by sliding the new control + in from the bottom and sliding the active control out the top. +*/ +enyo.kind({ + name: "enyo.TopBottomArranger", + kind: "LeftRightArranger", + dragProp: "ddy", + dragDirectionProp: "yDirection", + canDragProp: "vertical", + //* @protected + axisSize: "height", + offAxisSize: "width", + axisPosition: "top" +}); + +/** + _enyo.SpiralArranger_ is an enyo.Arranger that + arranges controls in a spiral. The active control is positioned on top and + the other controls are laid out in a spiral pattern below. + + Transitions between arrangements are handled by rotating the new control + up from below and rotating the active control down to the end of the spiral. +*/ +enyo.kind({ + name: "enyo.SpiralArranger", + kind: "Arranger", + //* Always go through incremental arrangements when transitioning + incrementalPoints: true, + //* The amount of space between successive controls + inc: 20, + size: function() { + var c$ = this.container.getPanels(); + var b = this.containerBounds; + var w = this.controlWidth = b.width/3; + var h = this.controlHeight = b.height/3; + for (var i=0, c; c=c$[i]; i++) { + c.setBounds({width: w, height: h}); + } + }, + arrange: function(inC, inName) { + var s = this.inc; + for (var i=0, l=inC.length, c; c=inC[i]; i++) { + var x = Math.cos(i/l * 2*Math.PI) * i * s + this.controlWidth; + var y = Math.sin(i/l * 2*Math.PI) * i * s + this.controlHeight; + this.arrangeControl(c, {left: x, top: y}); + } + }, + start: function() { + this.inherited(arguments); + var c$ = this.getOrderedControls(this.container.toIndex); + for (var i=0, c; c=c$[i]; i++) { + c.applyStyle("z-index", c$.length - i); + } + }, + calcArrangementDifference: function(inI0, inA0, inI1, inA1) { + return this.controlWidth; + }, + destroy: function() { + var c$ = this.container.getPanels(); + for (var i=0, c; c=c$[i]; i++) { + c.applyStyle("z-index", null); + enyo.Arranger.positionControl(c, {left: null, top: null}); + c.applyStyle("left", null); + c.applyStyle("top", null); + c.applyStyle("height", null); + c.applyStyle("width", null); + } + this.inherited(arguments); + } +}); + + +/** + _enyo.GridArranger_ is an enyo.Arranger that + arranges controls in a grid. The active control is positioned at the + top-left of the grid and the other controls are laid out from left to right + and then from top to bottom. + + Transitions between arrangements are handled by moving the active control to + the end of the grid and shifting the other controls to the left, or up to + the previous row, to fill the space. +*/ +enyo.kind({ + name: "enyo.GridArranger", + kind: "Arranger", + //* Always go through incremental arrangements when transitioning + incrementalPoints: true, + //* @public + //* Column width + colWidth: 100, + //* Column height + colHeight: 100, + size: function() { + var c$ = this.container.getPanels(); + var w=this.colWidth, h=this.colHeight; + for (var i=0, c; c=c$[i]; i++) { + c.setBounds({width: w, height: h}); + } + }, + arrange: function(inC, inIndex) { + var w=this.colWidth, h=this.colHeight; + var cols = Math.floor(this.containerBounds.width / w); + var c; + for (var y=0, i=0; i 2)) { + enyo.dom.accelerate(this, this.accelerated); + } + }, + axisChanged: function() { + var h = this.axis == "h"; + this.dragMoveProp = h ? "dx" : "dy"; + this.shouldDragProp = h ? "horizontal" : "vertical"; + this.transform = h ? "translateX" : "translateY"; + this.dimension = h ? "width" : "height"; + this.boundary = h ? "left" : "top"; + }, + setInlineStyles: function(inValue, inDimensions) { + var inBounds = {}; + + if (this.unitModifier) { + inBounds[this.boundary] = this.percentToPixels(inValue, this.unitModifier); + inBounds[this.dimension] = this.unitModifier; + this.setBounds(inBounds); + } else { + if (inDimensions) { + inBounds[this.dimension] = inDimensions; + } else { + inBounds[this.boundary] = inValue; + } + this.setBounds(inBounds, this.unit); + } + }, + valueChanged: function(inLast) { + var v = this.value; + if (this.isOob(v) && !this.isAnimating()) { + this.value = this.overMoving ? this.dampValue(v) : this.clampValue(v); + } + // FIXME: android cannot handle nested compositing well so apply acceleration only if needed + // desktop chrome doesn't like this code path so avoid... + if (enyo.platform.android > 2) { + if (this.value) { + if (inLast === 0 || inLast === undefined) { + enyo.dom.accelerate(this, this.accelerated); + } + } else { + enyo.dom.accelerate(this, false); + } + } + + // If platform supports transforms + if (this.canTransform) { + enyo.dom.transformValue(this, this.transform, this.value + this.unit); + // else update inline styles + } else { + this.setInlineStyles(this.value, false); + } + this.doChange(); + }, + getAnimator: function() { + return this.$.animator; + }, + isAtMin: function() { + return this.value <= this.calcMin(); + }, + isAtMax: function() { + return this.value >= this.calcMax(); + }, + calcMin: function() { + return this.min; + }, + calcMax: function() { + return this.max; + }, + clampValue: function(inValue) { + var min = this.calcMin(); + var max = this.calcMax(); + return Math.max(min, Math.min(inValue, max)); + }, + dampValue: function(inValue) { + return this.dampBound(this.dampBound(inValue, this.min, 1), this.max, -1); + }, + dampBound: function(inValue, inBoundary, inSign) { + var v = inValue; + if (v * inSign < inBoundary * inSign) { + v = inBoundary + (v - inBoundary) / 4; + } + return v; + }, + percentToPixels: function(value, dimension) { + return Math.floor((dimension / 100) * value); + }, + pixelsToPercent: function(value) { + var boundary = this.unitModifier ? this.getBounds()[this.dimension] : this.container.getBounds()[this.dimension]; + return (value / boundary) * 100; + }, + // dragging + shouldDrag: function(inEvent) { + return this.draggable && inEvent[this.shouldDragProp]; + }, + isOob: function(inValue) { + return inValue > this.calcMax() || inValue < this.calcMin(); + }, + dragstart: function(inSender, inEvent) { + if (this.shouldDrag(inEvent)) { + inEvent.preventDefault(); + this.$.animator.stop(); + inEvent.dragInfo = {}; + this.dragging = true; + this.drag0 = this.value; + this.dragd0 = 0; + return this.preventDragPropagation; + } + }, + drag: function(inSender, inEvent) { + if (this.dragging) { + inEvent.preventDefault(); + var d = this.canTransform ? inEvent[this.dragMoveProp] * this.kDragScalar : this.pixelsToPercent(inEvent[this.dragMoveProp]); + var v = this.drag0 + d; + var dd = d - this.dragd0; + this.dragd0 = d; + if (dd) { + inEvent.dragInfo.minimizing = dd < 0; + } + this.setValue(v); + return this.preventDragPropagation; + } + }, + dragfinish: function(inSender, inEvent) { + if (this.dragging) { + this.dragging = false; + this.completeDrag(inEvent); + inEvent.preventTap(); + return this.preventDragPropagation; + } + }, + completeDrag: function(inEvent) { + if (this.value !== this.calcMax() && this.value != this.calcMin()) { + this.animateToMinMax(inEvent.dragInfo.minimizing); + } + }, + // animation + isAnimating: function() { + return this.$.animator.isAnimating(); + }, + play: function(inStart, inEnd) { + this.$.animator.play({ + startValue: inStart, + endValue: inEnd, + node: this.hasNode() + }); + }, + //* @public + //* Animates to the given value. + animateTo: function(inValue) { + this.play(this.value, inValue); + }, + //* Animates to the minimum value. + animateToMin: function() { + this.animateTo(this.calcMin()); + }, + //* Animates to the maximum value. + animateToMax: function() { + this.animateTo(this.calcMax()); + }, + //* @protected + animateToMinMax: function(inMin) { + if (inMin) { + this.animateToMin(); + } else { + this.animateToMax(); + } + }, + animatorStep: function(inSender) { + this.setValue(inSender.value); + return true; + }, + animatorComplete: function(inSender) { + this.doAnimateFinish(inSender); + return true; + }, + //* @public + //* Toggles between min and max with animation. + toggleMinMax: function() { + this.animateToMinMax(!this.isAtMin()); + } +}); diff --git a/html/lib/layout/slideable/source/package.js b/html/lib/layout/slideable/source/package.js new file mode 100644 index 0000000..8c4ec43 --- /dev/null +++ b/html/lib/layout/slideable/source/package.js @@ -0,0 +1,3 @@ +enyo.depends( + "Slideable.js" +); diff --git a/html/lib/layout/tree/package.js b/html/lib/layout/tree/package.js new file mode 100644 index 0000000..1ec0cd0 --- /dev/null +++ b/html/lib/layout/tree/package.js @@ -0,0 +1,3 @@ +enyo.depends( + "source" +); \ No newline at end of file diff --git a/html/lib/layout/tree/source/Node.css b/html/lib/layout/tree/source/Node.css new file mode 100644 index 0000000..718a2e6 --- /dev/null +++ b/html/lib/layout/tree/source/Node.css @@ -0,0 +1,28 @@ +.enyo-node { + cursor: default; + padding: 4px; +} + +.enyo-node img { + vertical-align: middle; + padding-right: 6px; +} + +.enyo-node-box { + overflow: hidden; +} + +.enyo-node-client { + position: relative; +} + +.enyo-animate .enyo-node-box, .enyo-animate .enyo-node-client { + transition-property: height, top; + transition-duration: 0.2s, 0.2s; + -moz-transition-property: height, top; + -moz-transition-duration: 0.2s, 0.2s; + -o-transition-property: height, top; + -o-transition-duration: 0.2s, 0.2s; + -webkit-transition-property: height, top; + -webkit-transition-duration: 0.2s, 0.2s; +} diff --git a/html/lib/layout/tree/source/Node.js b/html/lib/layout/tree/source/Node.js new file mode 100644 index 0000000..ff79c9a --- /dev/null +++ b/html/lib/layout/tree/source/Node.js @@ -0,0 +1,269 @@ +/** + _enyo.Node_ is a control that creates structured trees based on Enyo's child + component hierarchy format, e.g.: + + {kind: "Node", icon: "images/folder-open.png", content: "Tree", + expandable: true, expanded: true, components: [ + {icon: "images/file.png", content: "Alpha"}, + {icon: "images/folder-open.png", content: "Bravo", + expandable: true, expanded: false, components: [ + {icon: "images/file.png", content: "Bravo-Alpha"}, + {icon: "images/file.png", content: "Bravo-Bravo"}, + {icon: "images/file.png", content: "Bravo-Charlie"} + ] + } + ] + } + + The default kind of components within a node is itself _enyo.Node_, so only + the top-level node of the tree needs to be explicitly defined as such. + + When an expandable tree node expands, an _onExpand_ event is sent; when it + is tapped, a _nodeTap_ event is sent. + + When the optional property _onlyIconExpands_ is set to true, expandable + nodes may only be opened by tapping the icon; tapping the content label + will fire the _nodeTap_ event, but will not expand the node. +*/ + +enyo.kind({ + name: "enyo.Node", + published: { + //* @public + //* Whether or not the Node is expandable and has child branches + expandable: false, + //* Open/closed state of the current Node + expanded: false, + //* Path to image to be used as the icon for this Node + icon: "", + /** + Optional flag that, when true, causes the Node to expand only when + the icon is tapped; not when the contents are tapped + */ + onlyIconExpands: false, + //* @protected + //* Adds or removes the Enyo-selected CSS class. + selected: false + }, + style: "padding: 0 0 0 16px;", + content: "Node", + defaultKind: "Node", + classes: "enyo-node", + components: [ + {name: "icon", kind: "Image", showing: false}, + {kind: "Control", name: "caption", Xtag: "span", style: "display: inline-block; padding: 4px;", allowHtml: true}, + {kind: "Control", name: "extra", tag: 'span', allowHtml: true} + ], + childClient: [ + {kind: "Control", name: "box", classes: "enyo-node-box", Xstyle: "border: 1px solid orange;", components: [ + {kind: "Control", name: "client", classes: "enyo-node-client", Xstyle: "border: 1px solid lightblue;"} + ]} + ], + handlers: { + ondblclick: "dblclick" + }, + events: { + //* @public + //* Fired when the Node is tapped + onNodeTap: "nodeTap", + //* Fired when the Node is double-clicked + onNodeDblClick: "nodeDblClick", + /** + Fired when the Node expands or contracts, as indicated by the + 'expanded' property in the event data + */ + onExpand: "nodeExpand", + //* Fired when the Node is destroyed + onDestroyed: "nodeDestroyed" + }, + // + //* @protected + create: function() { + this.inherited(arguments); + //this.expandedChanged(); + //this.levelChanged(); + this.selectedChanged(); + this.iconChanged(); + }, + destroy: function() { + this.doDestroyed(); + this.inherited(arguments); + }, + initComponents: function() { + // TODO: optimize to create the childClient on demand + //this.hasChildren = this.components; + if (this.expandable) { + this.kindComponents = this.kindComponents.concat(this.childClient); + } + this.inherited(arguments); + }, + // + contentChanged: function() { + //this.$.caption.setContent((this.expandable ? (this.expanded ? "-" : "+") : "") + this.content); + this.$.caption.setContent(this.content); + }, + iconChanged: function() { + this.$.icon.setSrc(this.icon); + this.$.icon.setShowing(Boolean(this.icon)); + }, + selectedChanged: function() { + this.addRemoveClass("enyo-selected", this.selected); + }, + rendered: function() { + this.inherited(arguments); + if (this.expandable && !this.expanded) { + this.quickCollapse(); + } + }, + // + addNodes: function(inNodes) { + this.destroyClientControls(); + for (var i=0, n; n=inNodes[i]; i++) { + this.createComponent(n); + } + this.$.client.render(); + }, + addTextNodes: function(inNodes) { + this.destroyClientControls(); + for (var i=0, n; n=inNodes[i]; i++) { + this.createComponent({content: n}); + } + this.$.client.render(); + }, + // + tap: function(inSender, inEvent) { + if(!this.onlyIconExpands) { + this.toggleExpanded(); + this.doNodeTap(); + } else { + if((inEvent.target==this.$.icon.hasNode())) { + this.toggleExpanded(); + } else { + this.doNodeTap(); + } + } + return true; + }, + dblclick: function(inSender, inEvent) { + this.doNodeDblClick(); + return true; + }, + // + toggleExpanded: function() { + this.setExpanded(!this.expanded); + }, + quickCollapse: function() { + this.removeClass("enyo-animate"); + this.$.box.applyStyle("height", "0"); + var h = this.$.client.getBounds().height; + this.$.client.setBounds({top: -h}); + }, + _expand: function() { + this.addClass("enyo-animate"); + var h = this.$.client.getBounds().height; + this.$.box.setBounds({height: h}); + this.$.client.setBounds({top: 0}); + setTimeout(enyo.bind(this, function() { + // things may have happened in the interim, make sure + // we only fix height if we are still expanded + if (this.expanded) { + this.removeClass("enyo-animate"); + this.$.box.applyStyle("height", "auto"); + } + }), 225); + }, + _collapse: function() { + // disable transitions + this.removeClass("enyo-animate"); + // fix the height of our box (rather than 'auto'), this + // gives webkit something to lerp from + var h = this.$.client.getBounds().height; + this.$.box.setBounds({height: h}); + // yield the thead so DOM can make those changes (without transitions) + setTimeout(enyo.bind(this, function() { + // enable transitions + this.addClass("enyo-animate"); + // shrink our box to 0 + this.$.box.applyStyle("height", "0"); + // slide the contents up + this.$.client.setBounds({top: -h}); + }), 25); + }, + expandedChanged: function(inOldExpanded) { + if (!this.expandable) { + this.expanded = false; + } else { + var event = {expanded: this.expanded}; + this.doExpand(event); + if (!event.wait) { + this.effectExpanded(); + } + } + }, + effectExpanded: function() { + if (this.$.client) { + if (!this.expanded) { + this._collapse(); + } else { + this._expand(); + } + } + //this.contentChanged(); + }/*, + // + // + levelChanged: function() { + this.applyStyle("padding-left", 16 + "px"); + }, + toggleChildren: function() { + if (this.$.list) { + this.$.list.setShowing(this.expanded); + } + }, + renderNodes: function(inNodes) { + var list = this.createComponent({name: "list", container: this}); + for (var i=0, n; n=inNodes[i]; i++) { + n.setLevel(this.level + 1); + n.setContainer(list); + n.render(); + } + list.render(); + }, + //* @public + addNodes: function(inNodes) { + this.renderNodes(inNodes); + this.toggleChildren(); + }, + removeNodes: function() { + if (this.$.list) { + this.$.list.destroy(); + } + }, + hasVisibleChildren: function() { + return this.expanded && this.$.list && this.$.list.controls.length > 0; + }, + fetchParent: function() { + return this.level > 0 && this.container.container; + }, + fetchChildren: function() { + return this.$.list && this.$.list.controls; + }, + fetchFirstChild: function() { + return this.$.list && this.$.list.controls[0]; + }, + fetchLastChild: function() { + return this.$.list && this.$.list.controls[this.$.list.controls.length-1]; + }, + fetchPrevSibling: function() { + var i = this.container.controls.indexOf(this); + return this.level > 0 && this.container.controls[i-1]; + }, + fetchNextSibling: function() { + var i = this.container.controls.indexOf(this); + return this.level > 0 && this.container.controls[i+1]; + }, + getVisibleBounds: function() { + return this.$.client.getBounds(); + } + */ +}); \ No newline at end of file diff --git a/html/lib/layout/tree/source/package.js b/html/lib/layout/tree/source/package.js new file mode 100644 index 0000000..5986bea --- /dev/null +++ b/html/lib/layout/tree/source/package.js @@ -0,0 +1,4 @@ +enyo.depends( + "Node.css", + "Node.js" +); \ No newline at end of file -- cgit v0.9.1