1 /** 2 * @fileOverview dom/style 3 * @author yiminghe@gmail.com,lifesinger@gmail.com 4 */ 5 KISSY.add('dom/style', function (S, DOM, UA, undefined) { 6 "use strict"; 7 var WINDOW = S.Env.host, 8 doc = WINDOW.document, 9 docElem = doc.documentElement, 10 isIE = UA['ie'], 11 STYLE = 'style', 12 FLOAT = 'float', 13 CSS_FLOAT = 'cssFloat', 14 STYLE_FLOAT = 'styleFloat', 15 WIDTH = 'width', 16 HEIGHT = 'height', 17 AUTO = 'auto', 18 DISPLAY = 'display', 19 OLD_DISPLAY = DISPLAY + S.now(), 20 NONE = 'none', 21 PARSEINT = parseInt, 22 RE_NUMPX = /^-?\d+(?:px)?$/i, 23 cssNumber = { 24 fillOpacity:1, 25 fontWeight:1, 26 lineHeight:1, 27 opacity:1, 28 orphans:1, 29 widows:1, 30 zIndex:1, 31 zoom:1 32 }, 33 rmsPrefix = /^-ms-/, 34 RE_DASH = /-([a-z])/ig, 35 CAMELCASE_FN = function (all, letter) { 36 return letter.toUpperCase(); 37 }, 38 // 考虑 ie9 ... 39 rupper = /([A-Z]|^ms)/g, 40 EMPTY = '', 41 DEFAULT_UNIT = 'px', 42 CUSTOM_STYLES = {}, 43 cssProps = {}, 44 defaultDisplay = {}; 45 46 // normalize reserved word float alternatives ("cssFloat" or "styleFloat") 47 if (docElem[STYLE][CSS_FLOAT] !== undefined) { 48 cssProps[FLOAT] = CSS_FLOAT; 49 } 50 else if (docElem[STYLE][STYLE_FLOAT] !== undefined) { 51 cssProps[FLOAT] = STYLE_FLOAT; 52 } 53 54 function camelCase(name) { 55 // fix #92, ms! 56 return name.replace(rmsPrefix, "ms-").replace(RE_DASH, CAMELCASE_FN); 57 } 58 59 var defaultDisplayDetectIframe, 60 defaultDisplayDetectIframeDoc; 61 62 // modified from jquery : bullet-proof method of getting default display 63 // fix domain problem in ie>6 , ie6 still access denied 64 function getDefaultDisplay(tagName) { 65 var body, 66 elem; 67 if (!defaultDisplay[ tagName ]) { 68 body = doc.body || doc.documentElement; 69 elem = doc.createElement(tagName); 70 DOM.prepend(elem, body); 71 var oldDisplay = DOM.css(elem, "display"); 72 body.removeChild(elem); 73 // If the simple way fails, 74 // get element's real default display by attaching it to a temp iframe 75 if (oldDisplay == "none" || oldDisplay == "") { 76 // No iframe to use yet, so create it 77 if (!defaultDisplayDetectIframe) { 78 defaultDisplayDetectIframe = doc.createElement("iframe"); 79 80 defaultDisplayDetectIframe.frameBorder = 81 defaultDisplayDetectIframe.width = 82 defaultDisplayDetectIframe.height = 0; 83 84 DOM.prepend(defaultDisplayDetectIframe, body); 85 var iframeSrc; 86 if (iframeSrc = DOM.getEmptyIframeSrc()) { 87 defaultDisplayDetectIframe.src = iframeSrc; 88 } 89 } else { 90 DOM.prepend(defaultDisplayDetectIframe, body); 91 } 92 93 // Create a cacheable copy of the iframe document on first call. 94 // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML 95 // document to it; WebKit & Firefox won't allow reusing the iframe document. 96 if (!defaultDisplayDetectIframeDoc || !defaultDisplayDetectIframe.createElement) { 97 98 try { 99 defaultDisplayDetectIframeDoc = defaultDisplayDetectIframe.contentWindow.document; 100 defaultDisplayDetectIframeDoc.write(( doc.compatMode === "CSS1Compat" ? "<!doctype html>" : "" ) 101 + "<html><head>" + 102 (UA['ie'] && DOM.isCustomDomain() ? 103 "<script>document.domain = '" + 104 doc.domain 105 + "';</script>" : "") 106 + 107 "</head><body>"); 108 defaultDisplayDetectIframeDoc.close(); 109 } catch (e) { 110 // ie6 need a breath , such as alert(8) or setTimeout; 111 // 同时需要同步,所以无解,勉强返回 112 return "block"; 113 } 114 } 115 116 elem = defaultDisplayDetectIframeDoc.createElement(tagName); 117 118 defaultDisplayDetectIframeDoc.body.appendChild(elem); 119 120 oldDisplay = DOM.css(elem, "display"); 121 122 body.removeChild(defaultDisplayDetectIframe); 123 } 124 125 // Store the correct default display 126 defaultDisplay[ tagName ] = oldDisplay; 127 } 128 129 return defaultDisplay[ tagName ]; 130 } 131 132 S.mix(DOM, 133 /** 134 * @lends DOM 135 */ 136 { 137 _camelCase:camelCase, 138 // _cssNumber:cssNumber, 139 _CUSTOM_STYLES:CUSTOM_STYLES, 140 _cssProps:cssProps, 141 _getComputedStyle:function (elem, name) { 142 var val = "", 143 computedStyle, 144 d = elem.ownerDocument; 145 146 name = name.replace(rupper, "-$1").toLowerCase(); 147 148 // https://github.com/kissyteam/kissy/issues/61 149 if (computedStyle = d.defaultView.getComputedStyle(elem, null)) { 150 val = computedStyle.getPropertyValue(name) || computedStyle[name]; 151 } 152 153 // 还没有加入到 document,就取行内 154 if (val == "" && !DOM.contains(d.documentElement, elem)) { 155 name = cssProps[name] || name; 156 val = elem[STYLE][name]; 157 } 158 159 return val; 160 }, 161 162 /** 163 * Get inline style property from the first element of matched elements 164 * or 165 * Set one or more CSS properties for the set of matched elements. 166 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements 167 * @param {String|Object} name A CSS property. or A map of property-value pairs to set. 168 * @param [val] A value to set for the property. 169 * @returns {undefined|String} 170 */ 171 style:function (selector, name, val) { 172 var els = DOM.query(selector), elem = els[0], i; 173 // supports hash 174 if (S.isPlainObject(name)) { 175 for (var k in name) { 176 for (i = els.length - 1; i >= 0; i--) { 177 style(els[i], k, name[k]); 178 } 179 } 180 return undefined; 181 } 182 if (val === undefined) { 183 var ret = ''; 184 if (elem) { 185 ret = style(elem, name, val); 186 } 187 return ret; 188 } else { 189 for (i = els.length - 1; i >= 0; i--) { 190 style(els[i], name, val); 191 } 192 } 193 return undefined; 194 }, 195 196 /** 197 * Get the computed value of a style property for the first element in the set of matched elements. 198 * or 199 * Set one or more CSS properties for the set of matched elements. 200 * @param {HTMLElement[]|String|HTMLElement|Element} selector 选择器或节点或节点数组 201 * @param {String|Object} name A CSS property. or A map of property-value pairs to set. 202 * @param [val] A value to set for the property. 203 * @returns {undefined|String} 204 */ 205 css:function (selector, name, val) { 206 var els = DOM.query(selector), 207 elem = els[0], 208 i; 209 // supports hash 210 if (S.isPlainObject(name)) { 211 for (var k in name) { 212 for (i = els.length - 1; i >= 0; i--) { 213 style(els[i], k, name[k]); 214 } 215 } 216 return undefined; 217 } 218 219 name = camelCase(name); 220 var hook = CUSTOM_STYLES[name]; 221 // getter 222 if (val === undefined) { 223 // supports css selector/Node/NodeList 224 var ret = ''; 225 if (elem) { 226 // If a hook was provided get the computed value from there 227 if (hook && "get" in hook && (ret = hook.get(elem, true)) !== undefined) { 228 } else { 229 ret = DOM._getComputedStyle(elem, name); 230 } 231 } 232 return ret === undefined ? '' : ret; 233 } 234 // setter 235 else { 236 for (i = els.length - 1; i >= 0; i--) { 237 style(els[i], name, val); 238 } 239 } 240 return undefined; 241 }, 242 243 /** 244 * Display the matched elements. 245 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements. 246 */ 247 show:function (selector) { 248 var els = DOM.query(selector), elem, i; 249 for (i = els.length - 1; i >= 0; i--) { 250 elem = els[i]; 251 elem[STYLE][DISPLAY] = DOM.data(elem, OLD_DISPLAY) || EMPTY; 252 253 // 可能元素还处于隐藏状态,比如 css 里设置了 display: none 254 if (DOM.css(elem, DISPLAY) === NONE) { 255 var tagName = elem.tagName.toLowerCase(), 256 old = getDefaultDisplay(tagName); 257 DOM.data(elem, OLD_DISPLAY, old); 258 elem[STYLE][DISPLAY] = old; 259 } 260 } 261 }, 262 263 /** 264 * Hide the matched elements. 265 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements. 266 */ 267 hide:function (selector) { 268 var els = DOM.query(selector), elem, i; 269 for (i = els.length - 1; i >= 0; i--) { 270 elem = els[i]; 271 var style = elem[STYLE], old = style[DISPLAY]; 272 if (old !== NONE) { 273 if (old) { 274 DOM.data(elem, OLD_DISPLAY, old); 275 } 276 style[DISPLAY] = NONE; 277 } 278 } 279 }, 280 281 /** 282 * Display or hide the matched elements. 283 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements. 284 */ 285 toggle:function (selector) { 286 var els = DOM.query(selector), elem, i; 287 for (i = els.length - 1; i >= 0; i--) { 288 elem = els[i]; 289 if (DOM.css(elem, DISPLAY) === NONE) { 290 DOM.show(elem); 291 } else { 292 DOM.hide(elem); 293 } 294 } 295 }, 296 297 /** 298 * Creates a stylesheet from a text blob of rules. 299 * These rules will be wrapped in a STYLE tag and appended to the HEAD of the document. 300 * @param {window} [refWin=window] Window which will accept this stylesheet 301 * @param {String} cssText The text containing the css rules 302 * @param {String} [id] An id to add to the stylesheet for later removal 303 */ 304 addStyleSheet:function (refWin, cssText, id) { 305 refWin = refWin || WINDOW; 306 if (S.isString(refWin)) { 307 id = cssText; 308 cssText = refWin; 309 refWin = WINDOW; 310 } 311 refWin = DOM.get(refWin); 312 var win = DOM._getWin(refWin), 313 doc = win.document, 314 elem; 315 316 if (id && (id = id.replace('#', EMPTY))) { 317 elem = DOM.get('#' + id, doc); 318 } 319 320 // 仅添加一次,不重复添加 321 if (elem) { 322 return; 323 } 324 325 elem = DOM.create('<style>', { id:id }, doc); 326 327 // 先添加到 DOM 树中,再给 cssText 赋值,否则 css hack 会失效 328 DOM.get('head', doc).appendChild(elem); 329 330 if (elem.styleSheet) { // IE 331 elem.styleSheet.cssText = cssText; 332 } else { // W3C 333 elem.appendChild(doc.createTextNode(cssText)); 334 } 335 }, 336 337 /** 338 * Make matched elements unselectable 339 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements. 340 */ 341 unselectable:function (selector) { 342 var _els = DOM.query(selector), elem, j; 343 for (j = _els.length - 1; j >= 0; j--) { 344 elem = _els[j]; 345 if (UA['gecko']) { 346 elem[STYLE]['MozUserSelect'] = 'none'; 347 } 348 else if (UA['webkit']) { 349 elem[STYLE]['KhtmlUserSelect'] = 'none'; 350 } else { 351 if (UA['ie'] || UA['opera']) { 352 var e, i = 0, 353 els = elem.getElementsByTagName("*"); 354 elem.setAttribute("unselectable", 'on'); 355 while (( e = els[ i++ ] )) { 356 switch (e.tagName.toLowerCase()) { 357 case 'iframe' : 358 case 'textarea' : 359 case 'input' : 360 case 'select' : 361 /* Ignore the above tags */ 362 break; 363 default : 364 e.setAttribute("unselectable", 'on'); 365 } 366 } 367 } 368 } 369 } 370 }, 371 372 /** 373 * Get the current computed width for the first element in the set of matched elements, including padding but not border. 374 * @function 375 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements 376 * @returns {Number} 377 */ 378 innerWidth:0, 379 /** 380 * Get the current computed height for the first element in the set of matched elements, including padding but not border. 381 * @function 382 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements 383 * @returns {Number} 384 */ 385 innerHeight:0, 386 /** 387 * Get the current computed width for the first element in the set of matched elements, including padding and border, and optionally margin. 388 * @function 389 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements 390 * @param {Boolean} [includeMargin] A Boolean indicating whether to include the element's margin in the calculation. 391 * @returns {Number} 392 */ 393 outerWidth:0, 394 /** 395 * Get the current computed height for the first element in the set of matched elements, including padding, border, and optionally margin. 396 * @function 397 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements 398 * @param {Boolean} [includeMargin] A Boolean indicating whether to include the element's margin in the calculation. 399 * @returns {Number} 400 */ 401 outerHeight:0, 402 /** 403 * Get the current computed width for the first element in the set of matched elements. 404 * or 405 * Set the CSS width of each element in the set of matched elements. 406 * @function 407 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements 408 * @param {String|Number} [value] 409 * An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). 410 * @returns {Number|undefined} 411 */ 412 width:0, 413 /** 414 * Get the current computed height for the first element in the set of matched elements. 415 * or 416 * Set the CSS height of each element in the set of matched elements. 417 * @function 418 * @param {HTMLElement[]|String|HTMLElement} selector Matched elements 419 * @param {String|Number} [value] 420 * An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). 421 * @returns {Number|undefined} 422 */ 423 height:0 424 }); 425 426 function capital(str) { 427 return str.charAt(0).toUpperCase() + str.substring(1); 428 } 429 430 S.each([WIDTH, HEIGHT], function (name) { 431 DOM["inner" + capital(name)] = function (selector) { 432 var el = DOM.get(selector); 433 return el && getWHIgnoreDisplay(el, name, "padding"); 434 }; 435 436 DOM["outer" + capital(name)] = function (selector, includeMargin) { 437 var el = DOM.get(selector); 438 return el && getWHIgnoreDisplay(el, name, includeMargin ? "margin" : "border"); 439 }; 440 441 DOM[name] = function (selector, val) { 442 var ret = DOM.css(selector, name, val); 443 if (ret) { 444 ret = parseFloat(ret); 445 } 446 return ret; 447 }; 448 }); 449 450 var cssShow = { position:"absolute", visibility:"hidden", display:"block" }; 451 452 /* 453 css height,width 永远都是计算值 454 */ 455 S.each(["height", "width"], function (name) { 456 /** 457 * @ignore 458 */ 459 CUSTOM_STYLES[ name ] = { 460 /** 461 * @ignore 462 */ 463 get:function (elem, computed) { 464 if (computed) { 465 return getWHIgnoreDisplay(elem, name) + "px"; 466 } 467 }, 468 set:function (elem, value) { 469 if (RE_NUMPX.test(value)) { 470 value = parseFloat(value); 471 if (value >= 0) { 472 return value + "px"; 473 } 474 } else { 475 return value; 476 } 477 } 478 }; 479 }); 480 481 S.each(["left", "top"], function (name) { 482 /** 483 * @ignore 484 */ 485 CUSTOM_STYLES[ name ] = { 486 get:function (elem, computed) { 487 if (computed) { 488 var val = DOM._getComputedStyle(elem, name), offset; 489 490 // 1. 当没有设置 style.left 时,getComputedStyle 在不同浏览器下,返回值不同 491 // 比如:firefox 返回 0, webkit/ie 返回 auto 492 // 2. style.left 设置为百分比时,返回值为百分比 493 // 对于第一种情况,如果是 relative 元素,值为 0. 如果是 absolute 元素,值为 offsetLeft - marginLeft 494 // 对于第二种情况,大部分类库都未做处理,属于“明之而不 fix”的保留 bug 495 if (val === AUTO) { 496 val = 0; 497 if (S.inArray(DOM.css(elem, 'position'), ['absolute', 'fixed'])) { 498 offset = elem[name === 'left' ? 'offsetLeft' : 'offsetTop']; 499 500 // old-ie 下,elem.offsetLeft 包含 offsetParent 的 border 宽度,需要减掉 501 if (isIE && doc['documentMode'] != 9 || UA['opera']) { 502 // 类似 offset ie 下的边框处理 503 // 如果 offsetParent 为 html ,需要减去默认 2 px == documentElement.clientTop 504 // 否则减去 borderTop 其实也是 clientTop 505 // http://msdn.microsoft.com/en-us/library/aa752288%28v=vs.85%29.aspx 506 // ie<9 注意有时候 elem.offsetParent 为 null ... 507 // 比如 DOM.append(DOM.create("<div class='position:absolute'></div>"),document.body) 508 offset -= elem.offsetParent && elem.offsetParent['client' + (name == 'left' ? 'Left' : 'Top')] 509 || 0; 510 } 511 val = offset - (PARSEINT(DOM.css(elem, 'margin-' + name)) || 0); 512 } 513 val += "px"; 514 } 515 return val; 516 } 517 } 518 }; 519 }); 520 521 function swap(elem, options, callback) { 522 var old = {}; 523 524 // Remember the old values, and insert the new ones 525 for (var name in options) { 526 old[ name ] = elem[STYLE][ name ]; 527 elem[STYLE][ name ] = options[ name ]; 528 } 529 530 callback.call(elem); 531 532 // Revert the old values 533 for (name in options) { 534 elem[STYLE][ name ] = old[ name ]; 535 } 536 } 537 538 function style(elem, name, val) { 539 var style; 540 if (elem.nodeType === 3 || elem.nodeType === 8 || !(style = elem[STYLE])) { 541 return undefined; 542 } 543 name = camelCase(name); 544 var ret, hook = CUSTOM_STYLES[name]; 545 name = cssProps[name] || name; 546 // setter 547 if (val !== undefined) { 548 // normalize unsetting 549 if (val === null || val === EMPTY) { 550 val = EMPTY; 551 } 552 // number values may need a unit 553 else if (!isNaN(Number(val)) && !cssNumber[name]) { 554 val += DEFAULT_UNIT; 555 } 556 if (hook && hook.set) { 557 val = hook.set(elem, val); 558 } 559 if (val !== undefined) { 560 // ie 无效值报错 561 try { 562 style[name] = val; 563 } catch (e) { 564 S.log("css set error :" + e); 565 } 566 // #80 fix,font-family 567 if (val === EMPTY && style.removeAttribute) { 568 style.removeAttribute(name); 569 } 570 } 571 if (!style.cssText) { 572 elem.removeAttribute('style'); 573 } 574 return undefined; 575 } 576 //getter 577 else { 578 // If a hook was provided get the non-computed value from there 579 if (hook && "get" in hook && (ret = hook.get(elem, false)) !== undefined) { 580 581 } else { 582 // Otherwise just get the value from the style object 583 ret = style[ name ]; 584 } 585 return ret === undefined ? "" : ret; 586 } 587 } 588 589 // fix #119 : https://github.com/kissyteam/kissy/issues/119 590 function getWHIgnoreDisplay(elem) { 591 var val, args = arguments; 592 // incase elem is window 593 // elem.offsetWidth === undefined 594 if (elem.offsetWidth !== 0) { 595 val = getWH.apply(undefined, args); 596 } else { 597 swap(elem, cssShow, function () { 598 val = getWH.apply(undefined, args); 599 }); 600 } 601 return val; 602 } 603 604 605 /** 606 * 得到元素的大小信息 607 * @param elem 608 * @param name 609 * @param {String} [extra] "padding" : (css width) + padding 610 * "border" : (css width) + padding + border 611 * "margin" : (css width) + padding + border + margin 612 */ 613 function getWH(elem, name, extra) { 614 if (S.isWindow(elem)) { 615 return name == WIDTH ? DOM.viewportWidth(elem) : DOM.viewportHeight(elem); 616 } else if (elem.nodeType == 9) { 617 return name == WIDTH ? DOM.docWidth(elem) : DOM.docHeight(elem); 618 } 619 var which = name === WIDTH ? ['Left', 'Right'] : ['Top', 'Bottom'], 620 val = name === WIDTH ? elem.offsetWidth : elem.offsetHeight; 621 622 if (val > 0) { 623 if (extra !== "border") { 624 S.each(which, function (w) { 625 if (!extra) { 626 val -= parseFloat(DOM.css(elem, "padding" + w)) || 0; 627 } 628 if (extra === "margin") { 629 val += parseFloat(DOM.css(elem, extra + w)) || 0; 630 } else { 631 val -= parseFloat(DOM.css(elem, "border" + w + "Width")) || 0; 632 } 633 }); 634 } 635 636 return val; 637 } 638 639 // Fall back to computed then uncomputed css if necessary 640 val = DOM._getComputedStyle(elem, name); 641 if (val == null || (Number(val)) < 0) { 642 val = elem.style[ name ] || 0; 643 } 644 // Normalize "", auto, and prepare for extra 645 val = parseFloat(val) || 0; 646 647 // Add padding, border, margin 648 if (extra) { 649 S.each(which, function (w) { 650 val += parseFloat(DOM.css(elem, "padding" + w)) || 0; 651 if (extra !== "padding") { 652 val += parseFloat(DOM.css(elem, "border" + w + "Width")) || 0; 653 } 654 if (extra === "margin") { 655 val += parseFloat(DOM.css(elem, extra + w)) || 0; 656 } 657 }); 658 } 659 660 return val; 661 } 662 663 return DOM; 664 }, { 665 requires:["dom/base", "ua"] 666 }); 667 668 /** 669 * 2011-12-21 670 * - backgroundPositionX, backgroundPositionY firefox/w3c 不支持 671 * - w3c 为准,这里不 fix 了 672 * 673 * 674 * 2011-08-19 675 * - 调整结构,减少耦合 676 * - fix css("height") == auto 677 * 678 * NOTES: 679 * - Opera 下,color 默认返回 #XXYYZZ, 非 rgb(). 目前 jQuery 等类库均忽略此差异,KISSY 也忽略。 680 * - Safari 低版本,transparent 会返回为 rgba(0, 0, 0, 0), 考虑低版本才有此 bug, 亦忽略。 681 * 682 * 683 * - getComputedStyle 在 webkit 下,会舍弃小数部分,ie 下会四舍五入,gecko 下直接输出 float 值。 684 * 685 * - color: blue 继承值,getComputedStyle, 在 ie 下返回 blue, opera 返回 #0000ff, 其它浏览器 686 * 返回 rgb(0, 0, 255) 687 * 688 * - 总之:要使得返回值完全一致是不大可能的,jQuery/ExtJS/KISSY 未“追求完美”。YUI3 做了部分完美处理,但 689 * 依旧存在浏览器差异。 690 */ 691