include('../view.js'); include('../geometry.js'); include('../utils.js'); include('../builder.js'); include('../dom.js'); include('observable.js'); include('styleable.js'); var ANCHOR_TOP = 1, ANCHOR_RIGHT = 2, ANCHOR_BOTTOM = 4, ANCHOR_LEFT = 8, ANCHOR_WIDTH = 16, ANCHOR_HEIGHT = 32; uki.view.declare('uki.view.Base', uki.view.Observable, uki.view.Styleable, function(Observable, Styleable) { var layoutId = 1; this.defaultCss = 'position:absolute;z-index:100;-moz-user-focus:none;'; /** * Base class for all uki views. * *

View creates and layouts dom nodes. uki.view.Base defines basic API for other views. * It also defines common layout algorithms.

* * Layout * *

View layout is defined by rectangle and anchors. * Rectangle is passed to constructor, anchors are set through the #anchors attribute.

* *

Rectangle defines initial position and size. Anchors specify how view will move and * resize when its parent is resized.

* * 2 phases of layout * *

Layout process has 2 phases. * First views rectangles are recalculated. This may happen several times before dom * is touched. This is done either explicitly through #rect attribute or through * #parentResized callbacks. * After rectangles are set #layout is called. This actually updates dom styles.

* * @example * uki({ view: 'Base', rect: '10 20 100 50', anchors: 'left top right' }) * // Creates Base view with initial x = 10px, y = 20px, width = 100px, height = 50px. * // When parent resizes x, y and height will stay the same. Width will resize with parent. * * * @see uki.view.Base#anchors * @constructor * @augments uki.view.Observable * @augments uki.view.Styleable * * @name uki.view.Base * @implements uki.view.Observable * @param {uki.geometry.Rect} rect initial position and size */ this.init = function(rect) { this._parentRect = this._rect = Rect.create(rect); this._setup(); uki.initNativeLayout(); this._createDom(); }; /**#@+ @memberOf uki.view.Base# */ /** @private */ this._setup = function() { uki.extend(this, { _anchors: 0, _parent: null, _visible: true, _needsLayout: true, _textSelectable: false, _styleH: 'left', _styleV: 'top', _firstLayout: true }); this.defaultCss += uki.theme.style('base'); }; /** * Get views container dom node. * @returns {Element} dom */ this.dom = function() { return this._dom; }; /* ------------------------------- Common settings --------------------------------*/ /** * Used for fast (on hash lookup) view searches: uki('#view_id'); * * @param {string=} id New id value * @returns {string|uki.view.Base} current id or self */ this.id = function(id) { if (id === undefined) return this._dom.id; if (this._dom.id) uki.unregisterId(this); this._dom.id = id; uki.registerId(this); return this; }; /** * Accessor for dom().className * @param {string=} className * * @returns {string|uki.view.Base} className or self */ uki.delegateProp(this, 'className', '_dom'); /** * Accessor for view visibility. * * @param {boolean=} state * @returns {boolean|uki.view.Base} current visibility state of self */ this.visible = function(state) { if (state === undefined) return this._dom.style.display != 'none'; this._dom.style.display = state ? 'block' : 'none'; return this; }; /** * Accessor for background attribute. * @param {string|uki.background.Base=} background * @returns {uki.background.Base|uki.view.Base} current background or self */ this.background = function(val) { if (val === undefined && !this._background && this.defaultBackground) this._background = this.defaultBackground(); if (val === undefined) return this._background; val = uki.background(val); if (val == this._background) return this; if (this._background) this._background.detach(this); val.attachTo(this); this._background = val; return this; }; /** * Accessor for default background attribute. * @name defaultBackground * @function * @returns {uki.background.Base} default background if not overridden through attribute */ this.defaultBackground = function() { return this._defaultBackground && uki.background(this._defaultBackground); }; /* ----------------------------- Container api ------------------------------*/ /** * Accessor attribute for parent view. When parent is set view appends its #dom * to parents #domForChild * * @param {?uki.view.Base=} parent * @returns {uki.view.Base} parent or self */ this.parent = function(parent) { if (parent === undefined) return this._parent; // if (this._parent) this._dom.parentNode.removeChild(this._dom); this._parent = parent; // if (this._parent) this._parent.domForChild(this).appendChild(this._dom); return this; }; /** * Accessor for childViews. @see uki.view.Container for implementation * @returns {Array.} */ this.childViews = function() { return []; }; /** * Reader for previous view * @returns {uki.view.Base} */ this.prevView = function() { if (!this.parent()) return null; return this.parent().childViews()[this._viewIndex - 1] || null; }; /** * Reader for next view * @returns {uki.view.Base} */ this.nextView = function() { if (!this.parent()) return null; return this.parent().childViews()[this._viewIndex + 1] || null; }; /* ----------------------------- Layout ------------------------------*/ /** * Sets or retrieves view's position and size. * * @param {string|uki.geometry.Rect} newRect * @returns {uki.view.Base} self */ this.rect = function(newRect) { if (newRect === undefined) return this._rect; newRect = Rect.create(newRect); this._parentRect = newRect; this._rect = this._normalizeRect(newRect); this._needsLayout = this._needsLayout || layoutId++; return this; }; /** * Set or get sides which the view should be attached to. * When a view is attached to a side the distance between this side and views border * will remain constant on resize. Anchor can be any combination of * "top", "right", "bottom", "left", "width" and "height". * If you set both "right" and "left" than "width" is assumed. * * Anchors are stored as a bit mask. Though its easier to set them using strings * * @function * @param {string|number} anchors * @returns {number|uki.view.Base} anchors or self */ this.anchors = uki.newProp('_anchors', function(anchors) { if (anchors.indexOf) { var tmp = 0; if (anchors.indexOf('right' ) > -1) tmp |= ANCHOR_RIGHT; if (anchors.indexOf('bottom' ) > -1) tmp |= ANCHOR_BOTTOM; if (anchors.indexOf('top' ) > -1) tmp |= ANCHOR_TOP; if (anchors.indexOf('left' ) > -1) tmp |= ANCHOR_LEFT; if (anchors.indexOf('width' ) > -1 || (tmp & ANCHOR_LEFT && tmp & ANCHOR_RIGHT)) tmp |= ANCHOR_WIDTH; if (anchors.indexOf('height' ) > -1 || (tmp & ANCHOR_BOTTOM && tmp & ANCHOR_TOP)) tmp |= ANCHOR_HEIGHT; anchors = tmp; } this._anchors = anchors; this._styleH = anchors & ANCHOR_LEFT ? 'left' : 'right'; this._styleV = anchors & ANCHOR_TOP ? 'top' : 'bottom'; }); /** * Returns rectangle for child layout. Usually equals to #rect. Though in some cases, * client rectangle my differ from #rect. Example uki.view.ScrollPane. * * @param {uki.view.Base} child * @returns {uki.geometry.Rect} */ this.rectForChild = function(child) { return this.rect(); }; /** * Updates dom to match #rect property. * * Layout is designed to minimize dom writes. If view is anchored to the right then * style.right is used, style.left for left anchor, etc. If browser supports this * both style.right and style.left are used. Otherwise style.width will be updated * manually on each resize. * * @fires event:layout * @see uki.dom.initNativeLayout */ this.layout = function() { this._layoutDom(this._rect); this._needsLayout = false; this.trigger('layout', {rect: this._rect, source: this}); this._firstLayout = false; }; this.layoutIfNeeded = function() { if (this._needsLayout && this.visible()) this.layout(); }; /** * @function uki.view.Base#minSize * @function uki.view.Base#maxSize */ uki.each(['min', 'max'], function(i, name) { var attr = name + 'Size', prop = '_' + attr; this[attr] = function(s) { if (s === undefined) return this[prop] || new Size(); this[prop] = Size.create(s); this.rect(this._parentRect); this._dom.style[name + 'Width'] = this[prop].width ? this[prop].width + PX : ''; this._dom.style[name + 'Height'] = this[prop].height ? this[prop].height + PX : ''; return this; }; }, this); /** * Resizes view when parent changes size according to anchors. * Called from parent view. Usually after parent's #rect is called. * * @param {uki.geometry.Rect} oldSize * @param {uki.geometry.Rect} newSize */ this.parentResized = function(oldSize, newSize) { var newRect = this._parentRect.clone(), dX = (newSize.width - oldSize.width) / ((this._anchors & ANCHOR_LEFT ^ ANCHOR_LEFT ? 1 : 0) + // flexible left (this._anchors & ANCHOR_WIDTH ? 1 : 0) + (this._anchors & ANCHOR_RIGHT ^ ANCHOR_RIGHT ? 1 : 0)), // flexible right dY = (newSize.height - oldSize.height) / ((this._anchors & ANCHOR_TOP ^ ANCHOR_TOP ? 1 : 0) + // flexible top (this._anchors & ANCHOR_HEIGHT ? 1 : 0) + (this._anchors & ANCHOR_BOTTOM ^ ANCHOR_BOTTOM ? 1 : 0)); // flexible right if (this._anchors & ANCHOR_LEFT ^ ANCHOR_LEFT) newRect.x += dX; if (this._anchors & ANCHOR_WIDTH) newRect.width += dX; if (this._anchors & ANCHOR_TOP ^ ANCHOR_TOP) newRect.y += dY; if (this._anchors & ANCHOR_HEIGHT) newRect.height += dY; this.rect(newRect); }; /** * Called when child changes it's size */ this.childResized = function(child) { // do nothing, extend in subviews }; /** * Resizes view to its contents. Contents size is determined by view. * View can be resized by width, height or both. This is specified through * autosizeStr param. * View will grow shrink according to its #anchors. * * @param {autosizeStr} autosize * @returns {uki.view.Base} self */ this.resizeToContents = function(autosizeStr) { var autosize = decodeAutosize(autosizeStr); if (0 == autosize) return this; var oldRect = this.rect(), newRect = this._calcRectOnContentResize(autosize); // if (newRect.eq(oldRect)) return this; // this.rect(newRect); this._rect = this._parentRect = newRect; this._needsLayout = true; return this; }; /** * Calculates view's contents size. Redefined in subclasses. * * @param {number} autosize Bitmask * @returns {uki.geometry.Rect} */ this.contentsSize = function(autosize) { return this.rect(); }; /** @private */ this._normalizeRect = function(rect) { if (this._minSize) { rect = new Rect(rect.x, rect.y, MAX(this._minSize.width, rect.width), MAX(this._minSize.height, rect.height)); } if (this._maxSize) { rect = new Rect(rect.x, rect.y, MIN(this._maxSize.width, rect.width), MIN(this._maxSize.height, rect.height)); } return rect; }; /** @ignore */ function decodeAutosize (autosizeStr) { if (!autosizeStr) return 0; var autosize = 0; if (autosizeStr.indexOf('width' ) > -1) autosize = autosize | ANCHOR_WIDTH; if (autosizeStr.indexOf('height') > -1) autosize = autosize | ANCHOR_HEIGHT; return autosize; } /** @private */ this._initBackgrounds = function() { if (this.background()) this.background().attachTo(this); }; /** @private */ this._calcRectOnContentResize = function(autosize) { var newSize = this.contentsSize( autosize ), oldSize = this.rect(); if (newSize.eq(oldSize)) return oldSize; // nothing changed // calculate where to resize var newRect = this.rect().clone(), dX = newSize.width - oldSize.width, dY = newSize.height - oldSize.height; if (autosize & ANCHOR_WIDTH) { if (this._anchors & ANCHOR_LEFT ^ ANCHOR_LEFT && this._anchors & ANCHOR_RIGHT ^ ANCHOR_RIGHT) { newRect.x -= dX/2; } else if (this._anchors & ANCHOR_LEFT ^ ANCHOR_LEFT) { newRect.x -= dX; } newRect.width += dX; } if (autosize & ANCHOR_HEIGHT) { if (this._anchors & ANCHOR_TOP ^ ANCHOR_TOP && this._anchors & ANCHOR_BOTTOM ^ ANCHOR_BOTTOM) { newRect.y -= dY/2; } else if (this._anchors & ANCHOR_TOP ^ ANCHOR_TOP) { newRect.y -= dY; } newRect.height += dY; } return newRect; }; /** @function @name uki.view.Base#width */ /** @function @name uki.view.Base#height */ /** @function @name uki.view.Base#minX */ /** @function @name uki.view.Base#maxX */ /** @function @name uki.view.Base#minY */ /** @function @name uki.view.Base#maxY */ /** @function @name uki.view.Base#left */ /** @function @name uki.view.Base#top */ uki.each(['width', 'height', 'minX', 'maxX', 'minY', 'maxY', 'x', 'y', 'left', 'top'], function(index, attr) { this[attr] = function(value) { if (value === undefined) return uki.attr(this.rect(), attr); uki.attr(this.rect(), attr, value); return this; }; }, this); /* ---------------------------------- Dom --------------------------------*/ /** * Called through a second layout pass when _dom should be created * @private */ this._createDom = function() { this._dom = uki.createElement('div', this.defaultCss); this._initClassName(); }; this._initClassName = function() { this._dom.className = this.typeName().replace(/\./g, '-'); }; /** * Called through a second layout pass when _dom is already created * @private */ this._layoutDom = function(rect) { var l = {}, s = uki.supportNativeLayout, relativeRect = this.parent().rectForChild(this); if (s && this._anchors & ANCHOR_LEFT && this._anchors & ANCHOR_RIGHT) { l.left = rect.x; l.right = relativeRect.width - rect.x - rect.width; } else { l.width = rect.width; l[this._styleH] = this._styleH == 'left' ? rect.x : relativeRect.width - rect.x - rect.width; } if (s && this._anchors & ANCHOR_TOP && this._anchors & ANCHOR_BOTTOM) { l.top = rect.y; l.bottom = relativeRect.height - rect.y - rect.height; } else { l.height = rect.height; l[this._styleV] = this._styleV == 'top' ? rect.y : relativeRect.height - rect.y - rect.height; } this._lastLayout = uki.dom.layout(this._dom.style, l, this._lastLayout); if (this._firstLayout) this._initBackgrounds(); return true; }; /** @private */ this._bindToDom = function(name) { if ('resize layout'.indexOf(name) > -1) return true; return uki.view.Observable._bindToDom.call(this, name); }; /**#@-*/ });