Created
October 13, 2015 01:50
-
-
Save johnpmorris/f0754a3f548dd7d6c7f3 to your computer and use it in GitHub Desktop.
css regions, out of the box by https://github.com/FremyCompany
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /*! CSS-REGIONS-POLYFILL - v3.0.0 - 2015-08-21 - https://github.com/FremyCompany/css-regions-polyfill - Copyright (c) 2015 François REMY; MIT-Licensed !*/ | |
| !(function() { 'use strict'; | |
| var module = { exports:{} }; | |
| var require = (function() { var modules = {}; var require = function(m) { return modules[m]; }; require.define = function(m) { modules[m]=module.exports; module.exports={}; }; return require; })(); | |
| //////////////////////////////////////// | |
| !(function(window, document) { "use strict"; | |
| // | |
| // some code for console polyfilling | |
| // | |
| if(!window.console) { | |
| window.console = { | |
| backlog: '', | |
| log: function(x) { this.backlog+=x+'\n'; if(window.debug) alert(x); }, | |
| dir: function(x) { try { | |
| var elm = function(e) { | |
| if(e.innerHTML) { | |
| return { | |
| tagName: e.tagName, | |
| className: e.className, | |
| id: e.id, | |
| innerHTML: e.innerHTML.substr(0,100) | |
| } | |
| } else { | |
| return { | |
| nodeName: e.nodeName, | |
| nodeValue: e.nodeValue | |
| } | |
| } | |
| }; | |
| var jsonify = function(o) { | |
| var seen=[]; | |
| var jso=JSON.stringify(o, function(k,v){ | |
| if (typeof v =='object') { | |
| if ( !seen.indexOf(v) ) { return '__cycle__'; } | |
| if ( v instanceof window.Node) { return elm(v); } | |
| seen.push(v); | |
| } return v; | |
| }); | |
| return jso; | |
| }; | |
| this.log(jsonify(x)); | |
| } catch(ex) { this.log(x) } }, | |
| warn: function(x) { this.log(x) }, | |
| error: function(x) { this.log("ERROR:"); this.log(x); } | |
| }; | |
| if(!window.onerror) { | |
| window.onerror = function() { | |
| console.log([].slice.call(arguments,0).join("\n")) | |
| }; | |
| } | |
| } | |
| // | |
| // this special console is used as a proxy emulating the CSS console of browsers | |
| // | |
| window.cssConsole = { | |
| enabled: (!!window.debug), warnEnabled: (true), | |
| log: function(x) { if(this.enabled) console.log(x) }, | |
| dir: function(x) { if(this.enabled) console.dir(x) }, | |
| warn: function(x) { if(this.warnEnabled) console.warn(x) }, | |
| error: function(x) { console.error(x); } | |
| } | |
| })(window, document); | |
| require.define('src/core/polyfill-dom-console.js'); | |
| //////////////////////////////////////// | |
| module.exports = (function(window, document) { "use strict"; | |
| require('src/core/polyfill-dom-console.js'); | |
| // | |
| // some other basic om code | |
| // | |
| var domEvents = { | |
| // | |
| // the following functions are about event cloning | |
| // | |
| cloneMouseEvent: function cloneMouseEvent(e) { | |
| var evt = document.createEvent("MouseEvent"); | |
| evt.initMouseEvent( | |
| e.type, | |
| e.canBubble||e.bubbles, | |
| e.cancelable, | |
| e.view, | |
| e.detail, | |
| e.screenX, | |
| e.screenY, | |
| e.clientX, | |
| e.clientY, | |
| e.ctrlKey, | |
| e.altKey, | |
| e.shiftKey, | |
| e.metaKey, | |
| e.button, | |
| e.relatedTarget | |
| ); | |
| return evt; | |
| }, | |
| cloneKeyboardEvent: function cloneKeyboardEvent(e) { | |
| // TODO: this doesn't work cross-browser... | |
| // see https://gist.github.com/termi/4654819/ for the huge code | |
| return domEvents.cloneCustomEvent(e); | |
| }, | |
| cloneCustomEvent: function cloneCustomEvent(e) { | |
| var ne = document.createEvent("CustomEvent"); | |
| ne.initCustomEvent(e.type, e.canBubble||e.bubbles, e.cancelable, "detail" in e ? e.detail : e); | |
| for(var prop in e) { | |
| try { | |
| if(e[prop] != ne[prop] && e[prop] != e.target) { | |
| try { ne[prop] = e[prop]; } | |
| catch (ex) { Object.defineProperty(ne,prop,{get:function() { return e[prop]} }) } | |
| } | |
| } catch(ex) {} | |
| } | |
| return ne; | |
| }, | |
| cloneEvent: function cloneEvent(e) { | |
| if(e instanceof MouseEvent) { | |
| return domEvents.cloneMouseEvent(e); | |
| } else if(e instanceof KeyboardEvent) { | |
| return domEvents.cloneKeyboardEvent(e); | |
| } else { | |
| return domEvents.cloneCustomEvent(e); | |
| } | |
| }, | |
| // | |
| // allows you to drop event support to any class easily | |
| // | |
| EventTarget: { | |
| implementsIn: function(eventClass, static_class) { | |
| if(!static_class && typeof(eventClass)=="function") eventClass=eventClass.prototype; | |
| eventClass.dispatchEvent = domEvents.EventTarget.prototype.dispatchEvent; | |
| eventClass.addEventListener = domEvents.EventTarget.prototype.addEventListener; | |
| eventClass.removeEventListener = domEvents.EventTarget.prototype.removeEventListener; | |
| }, | |
| prototype: {} | |
| } | |
| }; | |
| domEvents.EventTarget.prototype.addEventListener = function(eventType,f) { | |
| if(!this.eventListeners) this.eventListeners=[]; | |
| var ls = (this.eventListeners[eventType] || (this.eventListeners[eventType]=[])); | |
| if(ls.indexOf(f)==-1) { | |
| ls.push(f); | |
| } | |
| } | |
| domEvents.EventTarget.prototype.removeEventListener = function(eventType,f) { | |
| if(!this.eventListeners) this.eventListeners=[]; | |
| var ls = (this.eventListeners[eventType] || (this.eventListeners[eventType]=[])), i; | |
| if((i=ls.indexOf(f))!==-1) { | |
| ls.splice(i,1); | |
| } | |
| } | |
| domEvents.EventTarget.prototype.dispatchEvent = function(event_or_type) { | |
| if(!this.eventListeners) this.eventListeners=[]; | |
| // abort quickly when no listener has been set up | |
| if(typeof(event_or_type) == "string") { | |
| if(!this.eventListeners[event_or_type] || this.eventListeners[event_or_type].length==0) { | |
| return; | |
| } | |
| } else { | |
| if(!this.eventListeners[event_or_type.type] || this.eventListeners[event_or_type.type].length==0) { | |
| return; | |
| } | |
| } | |
| // convert the event | |
| var event = event_or_type; | |
| function setUpPropertyForwarding(e,ee,key) { | |
| Object.defineProperty(ee,key,{ | |
| get:function() { | |
| var v = e[key]; | |
| if(typeof(v)=="function") { | |
| return v.bind(e); | |
| } else { | |
| return v; | |
| } | |
| }, | |
| set:function(v) { | |
| e[key] = v; | |
| } | |
| }); | |
| } | |
| function setUpTarget(e,v) { | |
| try { Object.defineProperty(e,"target",{get:function() {return v}}); } | |
| catch(ex) {} | |
| finally { | |
| if(e.target !== v) { | |
| var ee = Object.create(Object.getPrototypeOf(e)); | |
| ee = setUpTarget(ee,v); | |
| for(key in e) { | |
| if(key != "target") setUpPropertyForwarding(e,ee,key); | |
| } | |
| return ee; | |
| } else { | |
| return e; | |
| } | |
| } | |
| } | |
| // try to set the target | |
| if(typeof(event)=="object") { | |
| try { event=setUpTarget(event,this); } catch(ex) {} | |
| } else if(typeof(event)=="string") { | |
| event = document.createEvent("CustomEvent"); | |
| event.initCustomEvent(event_or_type, /*canBubble:*/ true, /*cancelable:*/ false, /*detail:*/this); | |
| try { event=setUpTarget(event,this); } catch(ex) {} | |
| } else { | |
| throw new Error("dispatchEvent expect an Event object or a string containing the event type"); | |
| } | |
| // call all listeners | |
| var ls = (this.eventListeners[event.type] || (this.eventListeners[event.type]=[])); | |
| for(var i=ls.length; i--;) { | |
| try { | |
| ls[i](event); | |
| } catch(ex) { | |
| setImmediate(function() { throw ex; }); | |
| } | |
| } | |
| return event.isDefaultPrevented; | |
| } | |
| return domEvents; | |
| })(window, document); | |
| require.define('src/core/dom-events.js'); | |
| //////////////////////////////////////// | |
| // | |
| // note: this file is based on Tab Atkins's CSS Parser | |
| // please include him (@tabatkins) if you open any issue for this file | |
| // | |
| module.exports = (function(window, document) { "use strict"; | |
| // | |
| // exports | |
| // | |
| var cssSyntax = { | |
| tokenize: function(string) {/*filled later*/}, | |
| parse: function(tokens) {/*filled later*/} | |
| }; | |
| // | |
| // css tokenizer | |
| // | |
| // Add support for token lists (superclass of array) | |
| function TokenList() { | |
| var array = []; | |
| array.toCSSString=TokenListToCSSString; | |
| return array; | |
| } | |
| function TokenListToCSSString(sep) { | |
| if(sep) { | |
| return this.map(function(o) { return o.toCSSString(); }).join(sep); | |
| } else { | |
| return this.asCSSString || (this.asCSSString = ( | |
| this.map(function(o) { return o.toCSSString(); }).join("/**/") | |
| .replace(/( +\/\*\*\/ *| * | *\/\*\*\/ +)/g," ") | |
| .replace(/( +\/\*\*\/ *| * | *\/\*\*\/ +)/g," ") | |
| .replace(/(\!|\:|\;|\@|\.|\,|\*|\=|\&|\\|\/|\<|\>|\[|\{|\(|\]|\}|\)|\|)\/\*\*\//g,"$1") | |
| .replace(/\/\*\*\/(\!|\:|\;|\@|\.|\,|\*|\=|\&|\\|\/|\<|\>|\[|\{|\(|\]|\}|\)|\|)/g,"$1") | |
| )); | |
| } | |
| } | |
| cssSyntax.TokenList = TokenList; | |
| cssSyntax.TokenListToCSSString = TokenListToCSSString; | |
| function between(num, first, last) { return num >= first && num <= last; } | |
| function digit(code) { return between(code, 0x30,0x39); } | |
| function hexdigit(code) { return digit(code) || between(code, 0x41,0x46) || between(code, 0x61,0x66); } | |
| function uppercaseletter(code) { return between(code, 0x41,0x5a); } | |
| function lowercaseletter(code) { return between(code, 0x61,0x7a); } | |
| function letter(code) { return uppercaseletter(code) || lowercaseletter(code); } | |
| function nonascii(code) { return code >= 0x80; } | |
| function namestartchar(code) { return letter(code) || nonascii(code) || code == 0x5f; } | |
| function namechar(code) { return namestartchar(code) || digit(code) || code == 0x2d; } | |
| function nonprintable(code) { return between(code, 0,8) || code == 0xb || between(code, 0xe,0x1f) || code == 0x7f; } | |
| function newline(code) { return code == 0xa; } | |
| function whitespace(code) { return newline(code) || code == 9 || code == 0x20; } | |
| function badescape(code) { return newline(code) || isNaN(code); } | |
| var maximumallowedcodepoint = 0x10ffff; | |
| function InvalidCharacterError(message) { | |
| this.message = message; | |
| }; | |
| InvalidCharacterError.prototype = new Error; | |
| InvalidCharacterError.prototype.name = 'InvalidCharacterError'; | |
| function preprocess(str) { | |
| // Turn a string into an array of code points, | |
| // following the preprocessing cleanup rules. | |
| var codepoints = []; | |
| for(var i = 0; i < str.length; i++) { | |
| var code = str.charCodeAt(i); | |
| if(code == 0xd && str.charCodeAt(i+1) == 0xa) { | |
| code = 0xa; i++; | |
| } | |
| if(code == 0xd || code == 0xc) code = 0xa; | |
| if(code == 0x0) code = 0xfffd; | |
| if(between(code, 0xd800, 0xdbff) && between(str.charCodeAt(i+1), 0xdc00, 0xdfff)) { | |
| // Decode a surrogate pair into an astral codepoint. | |
| var lead = code - 0xd800; | |
| var trail = str.charCodeAt(i+1) - 0xdc00; | |
| code = Math.pow(2, 21) + lead * Math.pow(2, 10) + trail; | |
| } | |
| codepoints.push(code); | |
| } | |
| return codepoints; | |
| } | |
| function stringFromCode(code) { | |
| if(code <= 0xffff) return String.fromCharCode(code); | |
| // Otherwise, encode astral char as surrogate pair. | |
| code -= Math.pow(2, 21); | |
| var lead = Math.floor(code/Math.pow(2, 10)) + 0xd800; | |
| var trail = code % Math.pow(2, 10); + 0xdc00; | |
| return String.fromCharCode(lead) + String.fromCharCode(trail); | |
| } | |
| function tokenize(str) { | |
| str = preprocess(str); | |
| var i = -1; | |
| var tokens = new TokenList(); | |
| var code; | |
| // Line number information. | |
| var line = 0; | |
| var column = 0; | |
| // The only use of lastLineLength is in reconsume(). | |
| var lastLineLength = 0; | |
| var incrLineno = function() { | |
| line += 1; | |
| lastLineLength = column; | |
| column = 0; | |
| }; | |
| var locStart = {line:line, column:column}; | |
| var codepoint = function(i) { | |
| if(i >= str.length) { | |
| return -1; | |
| } | |
| return str[i]; | |
| } | |
| var next = function(num) { | |
| if(num === undefined) { num = 1; } | |
| if(num > 3) { throw "Spec Error: no more than three codepoints of lookahead."; } | |
| return codepoint(i+num); | |
| }; | |
| var consume = function(num) { | |
| if(num === undefined) | |
| num = 1; | |
| i += num; | |
| code = codepoint(i); | |
| if(newline(code)) incrLineno(); | |
| else column += num; | |
| //console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16)); | |
| return true; | |
| }; | |
| var reconsume = function() { | |
| i -= 1; | |
| if (newline(code)) { | |
| line -= 1; | |
| column = lastLineLength; | |
| } else { | |
| column -= 1; | |
| } | |
| locStart.line = line; | |
| locStart.column = column; | |
| return true; | |
| }; | |
| var eof = function(codepoint) { | |
| if(codepoint === undefined) codepoint = code; | |
| return codepoint == -1; | |
| }; | |
| var donothing = function() {}; | |
| var tokenizeerror = function() { console.log("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + ".");return true; }; | |
| var consumeAToken = function() { | |
| consumeComments(); | |
| consume(); | |
| if(whitespace(code)) { | |
| while(whitespace(next())) consume(); | |
| return new WhitespaceToken; | |
| } | |
| else if(code == 0x22) return consumeAStringToken(); | |
| else if(code == 0x23) { | |
| if(namechar(next()) || areAValidEscape(next(1), next(2))) { | |
| var token = new HashToken(); | |
| if(wouldStartAnIdentifier(next(1), next(2), next(3))) token.type = "id"; | |
| token.value = consumeAName(); | |
| return token; | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x24) { | |
| if(next() == 0x3d) { | |
| consume(); | |
| return new SuffixMatchToken(); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x27) return consumeAStringToken(); | |
| else if(code == 0x28) return new OpenParenToken(); | |
| else if(code == 0x29) return new CloseParenToken(); | |
| else if(code == 0x2a) { | |
| if(next() == 0x3d) { | |
| consume(); | |
| return new SubstringMatchToken(); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x2b) { | |
| if(startsWithANumber()) { | |
| reconsume(); | |
| return consumeANumericToken(); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x2c) return new CommaToken(); | |
| else if(code == 0x2d) { | |
| if(startsWithANumber()) { | |
| reconsume(); | |
| return consumeANumericToken(); | |
| } else if(next(1) == 0x2d && next(2) == 0x3e) { | |
| consume(2); | |
| return new CDCToken(); | |
| } else if(startsWithAnIdentifier()) { | |
| reconsume(); | |
| return consumeAnIdentlikeToken(); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x2e) { | |
| if(startsWithANumber()) { | |
| reconsume(); | |
| return consumeANumericToken(); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x3a) return new ColonToken; | |
| else if(code == 0x3b) return new SemicolonToken; | |
| else if(code == 0x3c) { | |
| if(next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) { | |
| consume(3); | |
| return new CDOToken(); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x40) { | |
| if(wouldStartAnIdentifier(next(1), next(2), next(3))) { | |
| return new AtKeywordToken(consumeAName()); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x5b) return new OpenSquareToken(); | |
| else if(code == 0x5c) { | |
| if(startsWithAValidEscape()) { | |
| reconsume(); | |
| return consumeAnIdentlikeToken(); | |
| } else { | |
| tokenizeerror(); | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x5d) return new CloseSquareToken(); | |
| else if(code == 0x5e) { | |
| if(next() == 0x3d) { | |
| consume(); | |
| return new PrefixMatchToken(); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x7b) return new OpenCurlyToken(); | |
| else if(code == 0x7c) { | |
| if(next() == 0x3d) { | |
| consume(); | |
| return new DashMatchToken(); | |
| } else if(next() == 0x7c) { | |
| consume(); | |
| return new ColumnToken(); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(code == 0x7d) return new CloseCurlyToken(); | |
| else if(code == 0x7e) { | |
| if(next() == 0x3d) { | |
| consume(); | |
| return new IncludeMatchToken(); | |
| } else { | |
| return new DelimToken(code); | |
| } | |
| } | |
| else if(digit(code)) { | |
| reconsume(); | |
| return consumeANumericToken(); | |
| } | |
| else if(namestartchar(code)) { | |
| reconsume(); | |
| return consumeAnIdentlikeToken(); | |
| } | |
| else if(eof()) return new EOFToken(); | |
| else return new DelimToken(code); | |
| }; | |
| var consumeComments = function() { | |
| while(next(1) == 0x2f && next(2) == 0x2a) { | |
| consume(2); | |
| while(true) { | |
| consume(); | |
| if(code == 0x2a && next() == 0x2f) { | |
| consume(); | |
| break; | |
| } else if(eof()) { | |
| tokenizeerror(); | |
| return; | |
| } | |
| } | |
| } | |
| }; | |
| var consumeANumericToken = function() { | |
| var num = consumeANumber(); | |
| if(wouldStartAnIdentifier(next(1), next(2), next(3))) { | |
| var token = new DimensionToken(); | |
| token.value = num.value; | |
| token.repr = num.repr; | |
| token.type = num.type; | |
| token.unit = consumeAName(); | |
| return token; | |
| } else if(next() == 0x25) { | |
| consume(); | |
| var token = new PercentageToken(); | |
| token.value = num.value; | |
| token.repr = num.repr; | |
| return token; | |
| } else { | |
| var token = new NumberToken(); | |
| token.value = num.value; | |
| token.repr = num.repr; | |
| token.type = num.type; | |
| return token; | |
| } | |
| }; | |
| var consumeAnIdentlikeToken = function() { | |
| var str = consumeAName(); | |
| if(str.toLowerCase() == "url" && next() == 0x28) { | |
| consume(); | |
| while(whitespace(next(1)) && whitespace(next(2))) consume(); | |
| if(next() == 0x22 || next() == 0x27) { | |
| return new FunctionToken(str); | |
| } else if(whitespace(next()) && (next(2) == 0x22 || next(2) == 0x27)) { | |
| return new FunctionToken(str); | |
| } else { | |
| return consumeAURLToken(); | |
| } | |
| } else if(next() == 0x28) { | |
| consume(); | |
| return new FunctionToken(str); | |
| } else { | |
| return new IdentifierToken(str); | |
| } | |
| }; | |
| var consumeAStringToken = function(endingCodePoint) { | |
| if(endingCodePoint === undefined) endingCodePoint = code; | |
| var string = ""; | |
| while(consume()) { | |
| if(code == endingCodePoint || eof()) { | |
| return new StringToken(string); | |
| } else if(newline(code)) { | |
| tokenizeerror(); | |
| reconsume(); | |
| return new BadStringToken(); | |
| } else if(code == 0x5c) { | |
| if(eof(next())) { | |
| donothing(); | |
| } else if(newline(next())) { | |
| consume(); | |
| } else { | |
| string += stringFromCode(consumeEscape()) | |
| } | |
| } else { | |
| string += stringFromCode(code); | |
| } | |
| } | |
| }; | |
| var consumeAURLToken = function() { | |
| var token = new URLToken(""); | |
| while(whitespace(next())) consume(); | |
| if(eof(next())) return token; | |
| while(consume()) { | |
| if(code == 0x29 || eof()) { | |
| return token; | |
| } else if(whitespace(code)) { | |
| while(whitespace(next())) consume(); | |
| if(next() == 0x29 || eof(next())) { | |
| consume(); | |
| return token; | |
| } else { | |
| consumeTheRemnantsOfABadURL(); | |
| return new BadURLToken(); | |
| } | |
| } else if(code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) { | |
| tokenizeerror(); | |
| consumeTheRemnantsOfABadURL(); | |
| return new BadURLToken(); | |
| } else if(code == 0x5c) { | |
| if(startsWithAValidEscape()) { | |
| token.value += stringFromCode(consumeEscape()); | |
| } else { | |
| tokenizeerror(); | |
| consumeTheRemnantsOfABadURL(); | |
| return new BadURLToken(); | |
| } | |
| } else { | |
| token.value += stringFromCode(code); | |
| } | |
| } | |
| }; | |
| var consumeEscape = function() { | |
| // Assume the the current character is the \ | |
| // and the next code point is not a newline. | |
| consume(); | |
| if(hexdigit(code)) { | |
| // Consume 1-6 hex digits | |
| var digits = [code]; | |
| for(var total = 0; total < 5; total++) { | |
| if(hexdigit(next())) { | |
| consume(); | |
| digits.push(code); | |
| } else { | |
| break; | |
| } | |
| } | |
| if(whitespace(next())) consume(); | |
| var value = parseInt(digits.map(function(x){return String.fromCharCode(x);}).join(''), 16); | |
| if( value > maximumallowedcodepoint ) value = 0xfffd; | |
| return value; | |
| } else if(eof()) { | |
| return 0xfffd; | |
| } else { | |
| return code; | |
| } | |
| }; | |
| var areAValidEscape = function(c1, c2) { | |
| if(c1 != 0x5c) return false; | |
| if(newline(c2)) return false; | |
| return true; | |
| }; | |
| var startsWithAValidEscape = function() { | |
| return areAValidEscape(code, next()); | |
| }; | |
| var wouldStartAnIdentifier = function(c1, c2, c3) { | |
| if(c1 == 0x2d) { | |
| return namestartchar(c2) || c2 == 0x2d || areAValidEscape(c2, c3); | |
| } else if(namestartchar(c1)) { | |
| return true; | |
| } else if(c1 == 0x5c) { | |
| return areAValidEscape(c1, c2); | |
| } else { | |
| return false; | |
| } | |
| }; | |
| var startsWithAnIdentifier = function() { | |
| return wouldStartAnIdentifier(code, next(1), next(2)); | |
| }; | |
| var wouldStartANumber = function(c1, c2, c3) { | |
| if(c1 == 0x2b || c1 == 0x2d) { | |
| if(digit(c2)) return true; | |
| if(c2 == 0x2e && digit(c3)) return true; | |
| return false; | |
| } else if(c1 == 0x2e) { | |
| if(digit(c2)) return true; | |
| return false; | |
| } else if(digit(c1)) { | |
| return true; | |
| } else { | |
| return false; | |
| } | |
| }; | |
| var startsWithANumber = function() { | |
| return wouldStartANumber(code, next(1), next(2)); | |
| }; | |
| var consumeAName = function() { | |
| var result = ""; | |
| while(consume()) { | |
| if(namechar(code)) { | |
| result += stringFromCode(code); | |
| } else if(startsWithAValidEscape()) { | |
| result += stringFromCode(consumeEscape()); | |
| } else { | |
| reconsume(); | |
| return result; | |
| } | |
| } | |
| }; | |
| var consumeANumber = function() { | |
| var repr = ''; | |
| var type = "integer"; | |
| if(next() == 0x2b || next() == 0x2d) { | |
| consume(); | |
| repr += stringFromCode(code); | |
| } | |
| while(digit(next())) { | |
| consume(); | |
| repr += stringFromCode(code); | |
| } | |
| if(next(1) == 0x2e && digit(next(2))) { | |
| consume(); | |
| repr += stringFromCode(code); | |
| consume(); | |
| repr += stringFromCode(code); | |
| type = "number"; | |
| while(digit(next())) { | |
| consume(); | |
| repr += stringFromCode(code); | |
| } | |
| } | |
| var c1 = next(1), c2 = next(2), c3 = next(3); | |
| if((c1 == 0x45 || c1 == 0x65) && digit(c2)) { | |
| consume(); | |
| repr += stringFromCode(code); | |
| consume(); | |
| repr += stringFromCode(code); | |
| type = "number"; | |
| while(digit(next())) { | |
| consume(); | |
| repr += stringFromCode(code); | |
| } | |
| } else if((c1 == 0x45 || c1 == 0x65) && (c2 == 0x2b || c2 == 0x2d) && digit(c3)) { | |
| consume(); | |
| repr += stringFromCode(code); | |
| consume(); | |
| repr += stringFromCode(code); | |
| consume(); | |
| repr += stringFromCode(code); | |
| type = "number"; | |
| while(digit(next())) { | |
| consume(); | |
| repr += stringFromCode(code); | |
| } | |
| } | |
| var value = convertAStringToANumber(repr); | |
| return {type:type, value:value, repr:repr}; | |
| }; | |
| var convertAStringToANumber = function(string) { | |
| // CSS's number rules are identical to JS, afaik. | |
| return +string; | |
| }; | |
| var consumeTheRemnantsOfABadURL = function() { | |
| while(consume()) { | |
| if(code == 0x2d || eof()) { | |
| return; | |
| } else if(startsWithAValidEscape()) { | |
| consumeEscape(); | |
| donothing(); | |
| } else { | |
| donothing(); | |
| } | |
| } | |
| }; | |
| var iterationCount = 0; | |
| while(!eof(next())) { | |
| tokens.push(consumeAToken()); | |
| if(iterationCount++ > str.length*2) throw new Error("The CSS Tokenizer is infinite-looping"); | |
| } | |
| return tokens; | |
| } | |
| function CSSParserToken() { return this; } | |
| CSSParserToken.prototype.toJSON = function() { | |
| return {token: this.tokenType}; | |
| } | |
| CSSParserToken.prototype.toString = function() { return this.tokenType; } | |
| CSSParserToken.prototype.toCSSString = function() { return ''+this; } | |
| function BadStringToken() { return this; } | |
| BadStringToken.prototype = new CSSParserToken; | |
| BadStringToken.prototype.tokenType = "BADSTRING"; | |
| BadStringToken.prototype.toCSSString = function() { return "'"; } | |
| function BadURLToken() { return this; } | |
| BadURLToken.prototype = new CSSParserToken; | |
| BadURLToken.prototype.tokenType = "BADURL"; | |
| BadURLToken.prototype.toCSSString = function() { return "url("; } | |
| function WhitespaceToken() { return this; } | |
| WhitespaceToken.prototype = new CSSParserToken; | |
| WhitespaceToken.prototype.tokenType = "WHITESPACE"; | |
| WhitespaceToken.prototype.toString = function() { return "WS"; } | |
| WhitespaceToken.prototype.toCSSString = function() { return " "; } | |
| function CDOToken() { return this; } | |
| CDOToken.prototype = new CSSParserToken; | |
| CDOToken.prototype.tokenType = "CDO"; | |
| CDOToken.prototype.toCSSString = function() { return "<!--"; } | |
| function CDCToken() { return this; } | |
| CDCToken.prototype = new CSSParserToken; | |
| CDCToken.prototype.tokenType = "CDC"; | |
| CDCToken.prototype.toCSSString = function() { return "-->"; } | |
| function ColonToken() { return this; } | |
| ColonToken.prototype = new CSSParserToken; | |
| ColonToken.prototype.tokenType = ":"; | |
| function SemicolonToken() { return this; } | |
| SemicolonToken.prototype = new CSSParserToken; | |
| SemicolonToken.prototype.tokenType = ";"; | |
| function CommaToken() { return this; } | |
| CommaToken.prototype = new CSSParserToken; | |
| CommaToken.prototype.tokenType = ","; | |
| CommaToken.prototype.value = ";"; // backwards-compat with DELIM token | |
| function GroupingToken() { return this; } | |
| GroupingToken.prototype = new CSSParserToken; | |
| function OpenCurlyToken() { this.value = "{"; this.mirror = "}"; return this; } | |
| OpenCurlyToken.prototype = new GroupingToken; | |
| OpenCurlyToken.prototype.tokenType = "{"; | |
| function CloseCurlyToken() { this.value = "}"; this.mirror = "{"; return this; } | |
| CloseCurlyToken.prototype = new GroupingToken; | |
| CloseCurlyToken.prototype.tokenType = "}"; | |
| function OpenSquareToken() { this.value = "["; this.mirror = "]"; return this; } | |
| OpenSquareToken.prototype = new GroupingToken; | |
| OpenSquareToken.prototype.tokenType = "["; | |
| function CloseSquareToken() { this.value = "]"; this.mirror = "["; return this; } | |
| CloseSquareToken.prototype = new GroupingToken; | |
| CloseSquareToken.prototype.tokenType = "]"; | |
| function OpenParenToken() { this.value = "("; this.mirror = ")"; return this; } | |
| OpenParenToken.prototype = new GroupingToken; | |
| OpenParenToken.prototype.tokenType = "("; | |
| function CloseParenToken() { this.value = ")"; this.mirror = "("; return this; } | |
| CloseParenToken.prototype = new GroupingToken; | |
| CloseParenToken.prototype.tokenType = ")"; | |
| function IncludeMatchToken() { return this; } | |
| IncludeMatchToken.prototype = new CSSParserToken; | |
| IncludeMatchToken.prototype.tokenType = "~="; | |
| function DashMatchToken() { return this; } | |
| DashMatchToken.prototype = new CSSParserToken; | |
| DashMatchToken.prototype.tokenType = "|="; | |
| function PrefixMatchToken() { return this; } | |
| PrefixMatchToken.prototype = new CSSParserToken; | |
| PrefixMatchToken.prototype.tokenType = "^="; | |
| function SuffixMatchToken() { return this; } | |
| SuffixMatchToken.prototype = new CSSParserToken; | |
| SuffixMatchToken.prototype.tokenType = "$="; | |
| function SubstringMatchToken() { return this; } | |
| SubstringMatchToken.prototype = new CSSParserToken; | |
| SubstringMatchToken.prototype.tokenType = "*="; | |
| function ColumnToken() { return this; } | |
| ColumnToken.prototype = new CSSParserToken; | |
| ColumnToken.prototype.tokenType = "||"; | |
| function EOFToken() { return this; } | |
| EOFToken.prototype = new CSSParserToken; | |
| EOFToken.prototype.tokenType = "EOF"; | |
| EOFToken.prototype.toCSSString = function() { return ""; } | |
| function DelimToken(code) { | |
| this.value = stringFromCode(code); | |
| return this; | |
| } | |
| DelimToken.prototype = new CSSParserToken; | |
| DelimToken.prototype.tokenType = "DELIM"; | |
| DelimToken.prototype.toString = function() { return "DELIM("+this.value+")"; } | |
| DelimToken.prototype.toCSSString = function() { | |
| return (this.value == "\\") ? "\\\n" : this.value; | |
| } | |
| function StringValuedToken() { return this; } | |
| StringValuedToken.prototype = new CSSParserToken; | |
| StringValuedToken.prototype.ASCIIMatch = function(str) { | |
| return this.value.toLowerCase() == str.toLowerCase(); | |
| } | |
| function IdentifierToken(val) { | |
| this.value = val; | |
| } | |
| IdentifierToken.prototype = new StringValuedToken; | |
| IdentifierToken.prototype.tokenType = "IDENT"; | |
| IdentifierToken.prototype.toString = function() { return "IDENT("+this.value+")"; } | |
| IdentifierToken.prototype.toCSSString = function() { | |
| return escapeIdent(this.value); | |
| } | |
| function FunctionToken(val) { | |
| this.value = val; | |
| this.mirror = ")"; | |
| } | |
| FunctionToken.prototype = new StringValuedToken; | |
| FunctionToken.prototype.tokenType = "FUNCTION"; | |
| FunctionToken.prototype.toString = function() { return "FUNCTION("+this.value+")"; } | |
| FunctionToken.prototype.toCSSString = function() { | |
| return escapeIdent(this.value) + "("; | |
| } | |
| function AtKeywordToken(val) { | |
| this.value = val; | |
| } | |
| AtKeywordToken.prototype = new StringValuedToken; | |
| AtKeywordToken.prototype.tokenType = "AT-KEYWORD"; | |
| AtKeywordToken.prototype.toString = function() { return "AT("+this.value+")"; } | |
| AtKeywordToken.prototype.toCSSString = function() { | |
| return "@" + escapeIdent(this.value); | |
| } | |
| function HashToken(val) { | |
| this.value = val; | |
| this.type = "unrestricted"; | |
| } | |
| HashToken.prototype = new StringValuedToken; | |
| HashToken.prototype.tokenType = "HASH"; | |
| HashToken.prototype.toString = function() { return "HASH("+this.value+")"; } | |
| HashToken.prototype.toCSSString = function() { | |
| var escapeValue = (this.type == "id") ? escapeIdent : escapeHash; | |
| return "#" + escapeValue(this.value); | |
| } | |
| function StringToken(val) { | |
| this.value = val; | |
| } | |
| StringToken.prototype = new StringValuedToken; | |
| StringToken.prototype.tokenType = "STRING"; | |
| StringToken.prototype.toString = function() { | |
| return '"' + escapeString(this.value) + '"'; | |
| } | |
| function URLToken(val) { | |
| this.value = val; | |
| } | |
| URLToken.prototype = new StringValuedToken; | |
| URLToken.prototype.tokenType = "URL"; | |
| URLToken.prototype.toString = function() { return "URL("+this.value+")"; } | |
| URLToken.prototype.toCSSString = function() { | |
| return 'url("' + escapeString(this.value) + '")'; | |
| } | |
| function NumberToken() { | |
| this.value = null; | |
| this.type = "integer"; | |
| this.repr = ""; | |
| } | |
| NumberToken.prototype = new CSSParserToken; | |
| NumberToken.prototype.tokenType = "NUMBER"; | |
| NumberToken.prototype.toString = function() { | |
| if(this.type == "integer") | |
| return "INT("+this.value+")"; | |
| return "NUMBER("+this.value+")"; | |
| } | |
| NumberToken.prototype.toJSON = function() { | |
| var json = this.constructor.prototype.constructor.prototype.toJSON.call(this); | |
| json.value = this.value; | |
| json.type = this.type; | |
| json.repr = this.repr; | |
| return json; | |
| } | |
| NumberToken.prototype.toCSSString = function() { return this.repr; }; | |
| function PercentageToken() { | |
| this.value = null; | |
| this.repr = ""; | |
| } | |
| PercentageToken.prototype = new CSSParserToken; | |
| PercentageToken.prototype.tokenType = "PERCENTAGE"; | |
| PercentageToken.prototype.toString = function() { return "PERCENTAGE("+this.value+")"; } | |
| PercentageToken.prototype.toCSSString = function() { return this.repr + "%"; } | |
| function DimensionToken() { | |
| this.value = null; | |
| this.type = "integer"; | |
| this.repr = ""; | |
| this.unit = ""; | |
| } | |
| DimensionToken.prototype = new CSSParserToken; | |
| DimensionToken.prototype.tokenType = "DIMENSION"; | |
| DimensionToken.prototype.toString = function() { return "DIM("+this.value+","+this.unit+")"; } | |
| DimensionToken.prototype.toCSSString = function() { | |
| var source = this.repr; | |
| var unit = escapeIdent(this.unit); | |
| if(unit[0].toLowerCase() == "e" && (unit[1] == "-" || between(unit.charCodeAt(1), 0x30, 0x39))) { | |
| // Unit is ambiguous with scinot | |
| // Remove the leading "e", replace with escape. | |
| unit = "\\65 " + unit.slice(1, unit.length); | |
| } | |
| return source+unit; | |
| } | |
| function escapeIdent(string) { | |
| string = ''+string; | |
| var result = ''; | |
| var firstcode = string.charCodeAt(0); | |
| for(var i = 0; i < string.length; i++) { | |
| var code = string.charCodeAt(i); | |
| if(code == 0x0) { | |
| throw new InvalidCharacterError('Invalid character: the input contains U+0000.'); | |
| } | |
| if( | |
| between(code, 0x1, 0x1f) || code == 0x7f || | |
| (i == 0 && between(code, 0x30, 0x39)) || | |
| (i == 1 && between(code, 0x30, 0x39) && firstcode == 0x2d) | |
| ) { | |
| result += '\\' + code.toString(16) + ' '; | |
| } else if( | |
| code >= 0x80 || | |
| code == 0x2d || | |
| code == 0x5f || | |
| between(code, 0x30, 0x39) || | |
| between(code, 0x41, 0x5a) || | |
| between(code, 0x61, 0x7a) | |
| ) { | |
| result += string[i]; | |
| } else { | |
| result += '\\' + string[i]; | |
| } | |
| } | |
| return result; | |
| } | |
| function escapeHash(string) { | |
| // Escapes the contents of "unrestricted"-type hash tokens. | |
| // Won't preserve the ID-ness of "id"-type hash tokens; | |
| // use escapeIdent() for that. | |
| string = ''+string; | |
| var result = ''; | |
| var firstcode = string.charCodeAt(0); | |
| for(var i = 0; i < string.length; i++) { | |
| var code = string.charCodeAt(i); | |
| if(code == 0x0) { | |
| throw new InvalidCharacterError('Invalid character: the input contains U+0000.'); | |
| } | |
| if( | |
| code >= 0x80 || | |
| code == 0x2d || | |
| code == 0x5f || | |
| between(code, 0x30, 0x39) || | |
| between(code, 0x41, 0x5a) || | |
| between(code, 0x61, 0x7a) | |
| ) { | |
| result += string[i]; | |
| } else { | |
| result += '\\' + code.toString(16) + ' '; | |
| } | |
| } | |
| return result; | |
| } | |
| function escapeString(string) { | |
| string = ''+string; | |
| var result = ''; | |
| for(var i = 0; i < string.length; i++) { | |
| var code = string.charCodeAt(i); | |
| if(code == 0x0) { | |
| throw new InvalidCharacterError('Invalid character: the input contains U+0000.'); | |
| } | |
| if(between(code, 0x1, 0x1f) || code == 0x7f) { | |
| result += '\\' + code.toString(16) + ' '; | |
| } else if(code == 0x22 || code == 0x5c) { | |
| result += '\\' + string[i]; | |
| } else { | |
| result += string[i]; | |
| } | |
| } | |
| return result; | |
| } | |
| // Exportation. | |
| cssSyntax.tokenize = tokenize; | |
| cssSyntax.IdentToken = IdentifierToken; | |
| cssSyntax.IdentifierToken = IdentifierToken; | |
| cssSyntax.FunctionToken = FunctionToken; | |
| cssSyntax.AtKeywordToken = AtKeywordToken; | |
| cssSyntax.HashToken = HashToken; | |
| cssSyntax.StringToken = StringToken; | |
| cssSyntax.BadStringToken = BadStringToken; | |
| cssSyntax.URLToken = URLToken; | |
| cssSyntax.BadURLToken = BadURLToken; | |
| cssSyntax.DelimToken = DelimToken; | |
| cssSyntax.NumberToken = NumberToken; | |
| cssSyntax.PercentageToken = PercentageToken; | |
| cssSyntax.DimensionToken = DimensionToken; | |
| cssSyntax.IncludeMatchToken = IncludeMatchToken; | |
| cssSyntax.DashMatchToken = DashMatchToken; | |
| cssSyntax.PrefixMatchToken = PrefixMatchToken; | |
| cssSyntax.SuffixMatchToken = SuffixMatchToken; | |
| cssSyntax.SubstringMatchToken = SubstringMatchToken; | |
| cssSyntax.ColumnToken = ColumnToken; | |
| cssSyntax.WhitespaceToken = WhitespaceToken; | |
| cssSyntax.CDOToken = CDOToken; | |
| cssSyntax.CDCToken = CDCToken; | |
| cssSyntax.ColonToken = ColonToken; | |
| cssSyntax.SemicolonToken = SemicolonToken; | |
| cssSyntax.CommaToken = CommaToken; | |
| cssSyntax.OpenParenToken = OpenParenToken; | |
| cssSyntax.CloseParenToken = CloseParenToken; | |
| cssSyntax.OpenSquareToken = OpenSquareToken; | |
| cssSyntax.CloseSquareToken = CloseSquareToken; | |
| cssSyntax.OpenCurlyToken = OpenCurlyToken; | |
| cssSyntax.CloseCurlyToken = CloseCurlyToken; | |
| cssSyntax.EOFToken = EOFToken; | |
| cssSyntax.CSSParserToken = CSSParserToken; | |
| cssSyntax.GroupingToken = GroupingToken; | |
| // | |
| // css parser | |
| // | |
| function TokenStream(tokens) { | |
| // Assume that tokens is an array. | |
| this.tokens = tokens; | |
| this.i = -1; | |
| } | |
| TokenStream.prototype.tokenAt = function(i) { | |
| if(i < this.tokens.length) | |
| return this.tokens[i]; | |
| return new EOFToken(); | |
| } | |
| TokenStream.prototype.consume = function(num) { | |
| if(num === undefined) num = 1; | |
| this.i += num; | |
| this.token = this.tokenAt(this.i); | |
| //console.log(this.i, this.token); | |
| return true; | |
| } | |
| TokenStream.prototype.next = function() { | |
| return this.tokenAt(this.i+1); | |
| } | |
| TokenStream.prototype.reconsume = function() { | |
| this.i--; | |
| } | |
| function parseerror(s, msg) { | |
| console.log("Parse error at token " + s.i + ": " + s.token + ".\n" + msg); | |
| return true; | |
| } | |
| function donothing(){ return true; }; | |
| function consumeAListOfRules(s, topLevel) { | |
| var rules = new TokenList(); | |
| var rule; | |
| while(s.consume()) { | |
| if(s.token instanceof WhitespaceToken) { | |
| continue; | |
| } else if(s.token instanceof EOFToken) { | |
| return rules; | |
| } else if(s.token instanceof CDOToken || s.token instanceof CDCToken) { | |
| if(topLevel == "top-level") continue; | |
| s.reconsume(); | |
| if(rule = consumeAStyleRule(s)) rules.push(rule); | |
| } else if(s.token instanceof AtKeywordToken) { | |
| s.reconsume(); | |
| if(rule = consumeAnAtRule(s)) rules.push(rule); | |
| } else { | |
| s.reconsume(); | |
| if(rule = consumeAStyleRule(s)) rules.push(rule); | |
| } | |
| } | |
| } | |
| function consumeAnAtRule(s) { | |
| s.consume(); | |
| var rule = new AtRule(s.token.value); | |
| while(s.consume()) { | |
| if(s.token instanceof SemicolonToken || s.token instanceof EOFToken) { | |
| return rule; | |
| } else if(s.token instanceof OpenCurlyToken) { | |
| rule.value = consumeASimpleBlock(s); | |
| return rule; | |
| } else if(s.token instanceof SimpleBlock && s.token.name == "{") { | |
| rule.value = s.token; | |
| return rule; | |
| } else { | |
| s.reconsume(); | |
| rule.prelude.push(consumeAComponentValue(s)); | |
| } | |
| } | |
| } | |
| function consumeAStyleRule(s) { | |
| var rule = new StyleRule(); | |
| while(s.consume()) { | |
| if(s.token instanceof EOFToken) { | |
| parseerror(s, "Hit EOF when trying to parse the prelude of a qualified rule."); | |
| return; | |
| } else if(s.token instanceof OpenCurlyToken) { | |
| rule.value = consumeASimpleBlock(s); | |
| return rule; | |
| } else if(s.token instanceof SimpleBlock && s.token.name == "{") { | |
| rule.value = s.token; | |
| return rule; | |
| } else { | |
| s.reconsume(); | |
| rule.prelude.push(consumeAComponentValue(s)); | |
| } | |
| } | |
| } | |
| function consumeAListOfDeclarations(s) { | |
| var decls = new TokenList(); | |
| while(s.consume()) { | |
| if(s.token instanceof WhitespaceToken || s.token instanceof SemicolonToken) { | |
| donothing(); | |
| } else if(s.token instanceof EOFToken) { | |
| return decls; | |
| } else if(s.token instanceof AtKeywordToken) { | |
| s.reconsume(); | |
| decls.push(consumeAnAtRule(s)); | |
| } else if(s.token instanceof IdentifierToken) { | |
| var temp = [s.token]; | |
| while(!(s.next() instanceof SemicolonToken || s.next() instanceof EOFToken)) | |
| temp.push(consumeAComponentValue(s)); | |
| var decl; | |
| if(decl = consumeADeclaration(new TokenStream(temp))) decls.push(decl); | |
| } else { | |
| parseerror(s); | |
| s.reconsume(); | |
| while(!(s.next() instanceof SemicolonToken || s.next() instanceof EOFToken)) | |
| consumeAComponentValue(s); | |
| } | |
| } | |
| } | |
| function consumeADeclaration(s) { | |
| // Assumes that the next input token will be an ident token. | |
| s.consume(); | |
| var decl = new Declaration(s.token.value); | |
| while(s.next() instanceof WhitespaceToken) s.consume(); | |
| if(!(s.next() instanceof ColonToken)) { | |
| parseerror(s); | |
| return; | |
| } else { | |
| s.consume(); | |
| } | |
| while(!(s.next() instanceof EOFToken)) { | |
| decl.value.push(consumeAComponentValue(s)); | |
| } | |
| var foundImportant = false; | |
| for(var i = decl.value.length - 1; i >= 0; i--) { | |
| if(decl.value[i] instanceof WhitespaceToken) { | |
| continue; | |
| } else if(decl.value[i] instanceof IdentifierToken && decl.value[i].ASCIIMatch("important")) { | |
| foundImportant = true; | |
| } else if(foundImportant && decl.value[i] instanceof DelimToken && decl.value[i].value == "!") { | |
| decl.value.splice(i, decl.value.length); | |
| decl.important = true; | |
| break; | |
| } else { | |
| break; | |
| } | |
| } | |
| return decl; | |
| } | |
| function consumeAComponentValue(s) { | |
| s.consume(); | |
| if(s.token instanceof OpenCurlyToken || s.token instanceof OpenSquareToken || s.token instanceof OpenParenToken) | |
| return consumeASimpleBlock(s); | |
| if(s.token instanceof FunctionToken) | |
| return consumeAFunction(s); | |
| return s.token; | |
| } | |
| function consumeASimpleBlock(s) { | |
| var mirror = s.token.mirror; | |
| var block = new SimpleBlock(s.token.value); | |
| while(s.consume()) { | |
| if(s.token instanceof EOFToken || (s.token instanceof GroupingToken && s.token.value == mirror)) | |
| return block; | |
| else { | |
| s.reconsume(); | |
| block.value.push(consumeAComponentValue(s)); | |
| } | |
| } | |
| } | |
| function consumeAFunction(s) { | |
| var func = new Func(s.token.value); | |
| while(s.consume()) { | |
| if(s.token instanceof EOFToken || s.token instanceof CloseParenToken) | |
| return func; | |
| else { | |
| s.reconsume(); | |
| func.value.push(consumeAComponentValue(s)); | |
| } | |
| } | |
| } | |
| function normalizeInput(input) { | |
| if(typeof input == "string") | |
| return new TokenStream(tokenize(input)); | |
| if(input instanceof TokenStream) | |
| return input; | |
| if(input.length !== undefined) | |
| return new TokenStream(input); | |
| else throw SyntaxError(input); | |
| } | |
| function parseAStylesheet(s) { | |
| s = normalizeInput(s); | |
| var sheet = new Stylesheet(); | |
| sheet.value = consumeAListOfRules(s, "top-level"); | |
| return sheet; | |
| } | |
| function parseAListOfRules(s) { | |
| s = normalizeInput(s); | |
| return consumeAListOfRules(s); | |
| } | |
| function parseARule(s) { | |
| s = normalizeInput(s); | |
| while(s.next() instanceof WhitespaceToken) s.consume(); | |
| if(s.next() instanceof EOFToken) throw SyntaxError(); | |
| if(s.next() instanceof AtKeywordToken) { | |
| var rule = consumeAnAtRule(s); | |
| } else { | |
| var rule = consumeAStyleRule(s); | |
| if(!rule) throw SyntaxError(); | |
| } | |
| while(s.next() instanceof WhitespaceToken) s.consume(); | |
| if(s.next() instanceof EOFToken) | |
| return rule; | |
| throw SyntaxError(); | |
| } | |
| function parseADeclaration(s) { | |
| s = normalizeInput(s); | |
| while(s.next() instanceof WhitespaceToken) s.consume(); | |
| if(!(s.next() instanceof IdentifierToken)) throw SyntaxError(); | |
| var decl = consumeADeclaration(s); | |
| if(!decl) { throw new SyntaxError() } | |
| return decl; | |
| } | |
| function parseAListOfDeclarations(s) { | |
| s = normalizeInput(s); | |
| return consumeAListOfDeclarations(s); | |
| } | |
| function parseAComponentValue(s) { | |
| s = normalizeInput(s); | |
| while(s.next() instanceof WhitespaceToken) s.consume(); | |
| if(s.next() instanceof EOFToken) throw SyntaxError(); | |
| var val = consumeAComponentValue(s); | |
| if(!val) throw SyntaxError(); | |
| while(s.next() instanceof WhitespaceToken) s.consume(); | |
| if(!(s.next() instanceof EOFToken)) throw new SyntaxError(); | |
| return val; | |
| } | |
| function parseAListOfComponentValues(s) { | |
| s = normalizeInput(s); | |
| var vals = new TokenList(); | |
| while(true) { | |
| var val = consumeAComponentValue(s); | |
| if(val instanceof EOFToken) | |
| return vals | |
| else | |
| vals.push(val); | |
| } | |
| } | |
| function parseACommaSeparatedListOfComponentValues(s) { | |
| s = normalizeInput(s); | |
| var listOfCVLs = new TokenList(); | |
| while(true) { | |
| var vals = new TokenList(); | |
| while(true) { | |
| var val = consumeAComponentValue(s); | |
| if(val instanceof EOFToken) { | |
| listOfCVLs.push(vals); | |
| return listOfCVLs; | |
| } else if(val instanceof CommaToken) { | |
| listOfCVLs.push(vals); | |
| break; | |
| } else { | |
| vals.push(val); | |
| } | |
| } | |
| } | |
| } | |
| function CSSParserRule() { return this; } | |
| CSSParserRule.prototype.toString = function(indent) { | |
| return JSON.stringify(this,null,indent); | |
| } | |
| function Stylesheet() { | |
| this.value = new TokenList(); | |
| return this; | |
| } | |
| Stylesheet.prototype = new CSSParserRule; | |
| Stylesheet.prototype.type = "STYLESHEET"; | |
| Stylesheet.prototype.toCSSString = function() { return this.value.toCSSString("\n"); } | |
| function AtRule(name) { | |
| this.name = name; | |
| this.prelude = new TokenList(); | |
| this.value = null; | |
| return this; | |
| } | |
| AtRule.prototype = new CSSParserRule; | |
| AtRule.prototype.toCSSString = function() { | |
| if(this.value) { | |
| return "@" + escapeIdent(this.name) + " " + this.prelude.toCSSString() + this.value.toCSSString(); | |
| } else { | |
| return "@" + escapeIdent(this.name) + " " + this.prelude.toCSSString() + '; '; | |
| } | |
| } | |
| AtRule.prototype.toStylesheet = function() { | |
| return this.asStylesheet || (this.asStylesheet = this.value ? parseAStylesheet(this.value.value) : new Stylesheet()); | |
| } | |
| function StyleRule() { | |
| this.prelude = new TokenList(); this.selector = this.prelude; | |
| this.value = null; | |
| return this; | |
| } | |
| StyleRule.prototype = new CSSParserRule; | |
| StyleRule.prototype.type = "STYLE-RULE"; | |
| StyleRule.prototype.toCSSString = function() { | |
| return this.prelude.toCSSString() + this.value.toCSSString(); | |
| } | |
| StyleRule.prototype.getSelector = function() { | |
| return this.prelude; | |
| } | |
| StyleRule.prototype.getDeclarations = function() { | |
| if(!(this.value instanceof SimpleBlock)) { return new TokenList(); } | |
| var value = this.value.value; return parseAListOfDeclarations(value); | |
| } | |
| function Declaration(name) { | |
| this.name = name; | |
| this.value = new TokenList(); | |
| this.important = false; | |
| return this; | |
| } | |
| Declaration.prototype = new CSSParserRule; | |
| Declaration.prototype.type = "DECLARATION"; | |
| Declaration.prototype.toCSSString = function() { | |
| return this.name + ':' + this.value.toCSSString() + '; '; | |
| } | |
| function SimpleBlock(type) { | |
| this.name = type; | |
| this.value = new TokenList(); | |
| return this; | |
| } | |
| SimpleBlock.prototype = new CSSParserRule; | |
| SimpleBlock.prototype.type = "BLOCK"; | |
| SimpleBlock.prototype.toCSSString = function() { | |
| switch(this.name) { | |
| case "(": | |
| return "(" + this.value.toCSSString() + ")"; | |
| case "[": | |
| return "[" + this.value.toCSSString() + "]"; | |
| case "{": | |
| return "{" + this.value.toCSSString() + "}"; | |
| default: //best guess | |
| return this.name + this.value.toCSSString() + this.name; | |
| } | |
| } | |
| function Func(name) { | |
| this.name = name; | |
| this.value = new TokenList(); | |
| return this; | |
| } | |
| Func.prototype = new CSSParserRule; | |
| Func.prototype.type = "FUNCTION"; | |
| Func.prototype.toCSSString = function() { | |
| return this.name+'('+this.value.toCSSString()+')'; | |
| } | |
| Func.prototype.getArguments = function() { | |
| var args = new TokenList(); var arg = new TokenList(); var value = this.value; | |
| for(var i = 0; i<value.length; i++) { | |
| if(value[i].tokenType == ',') { | |
| args.push(arg); arg = new TokenList(); | |
| } else { | |
| arg.push(value[i]) | |
| } | |
| } | |
| if(args.length > 0 || arg.length > 0) { args.push(arg); } | |
| return args; | |
| } | |
| function FuncArg() { | |
| this.value = new TokenList(); | |
| return this; | |
| } | |
| FuncArg.prototype = new CSSParserRule; | |
| FuncArg.prototype.type = "FUNCTION-ARG"; | |
| FuncArg.prototype.toCSSString = function() { | |
| return this.value.toCSSString()+', '; | |
| } | |
| // Exportation. | |
| cssSyntax.CSSParserRule = CSSParserRule; | |
| cssSyntax.Stylesheet = Stylesheet; | |
| cssSyntax.AtRule = AtRule; | |
| cssSyntax.StyleRule = StyleRule; | |
| cssSyntax.Declaration = Declaration; | |
| cssSyntax.SimpleBlock = SimpleBlock; | |
| cssSyntax.Func = Func; | |
| cssSyntax.parseAStylesheet = parseAStylesheet; | |
| cssSyntax.parseAListOfRules = parseAListOfRules; | |
| cssSyntax.parseARule = parseARule; | |
| cssSyntax.parseADeclaration = parseADeclaration; | |
| cssSyntax.parseAListOfDeclarations = parseAListOfDeclarations; | |
| cssSyntax.parseAComponentValue = parseAComponentValue; | |
| cssSyntax.parseAListOfComponentValues = parseAListOfComponentValues; | |
| cssSyntax.parseACommaSeparatedListOfComponentValues = parseACommaSeparatedListOfComponentValues; | |
| cssSyntax.parse = parseAStylesheet; | |
| cssSyntax.parseCSSValue = parseAListOfComponentValues; | |
| return cssSyntax; | |
| }()); | |
| require.define('src/core/css-syntax.js'); | |
| //////////////////////////////////////// | |
| void function() { | |
| // request animation frame | |
| var vendors = ['webkit', 'moz', 'ms', 'o']; | |
| for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) { | |
| var vp = vendors[i]; | |
| window.requestAnimationFrame = window[vp+'RequestAnimationFrame']; | |
| window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame'] || window[vp+'CancelRequestAnimationFrame']); | |
| } | |
| if (!window.requestAnimationFrame || !window.cancelAnimationFrame) { | |
| // tick every 16ms | |
| var listener_index = 0; var listeners = []; var tmp = []; var tick = function() { | |
| var now = +(new Date()); var callbacks = listeners; listeners = tmp; | |
| for(var i = 0; i<callbacks.length; i++) { callbacks[i](now); } | |
| listener_index += callbacks.length; callbacks.length = 0; tmp = callbacks; | |
| setTimeout(tick, 16); | |
| }; tick(); | |
| // add a listener | |
| window.requestAnimationFrame = function(callback) { | |
| return listener_index + listeners.push(callback); | |
| }; | |
| // remove a listener | |
| window.cancelAnimationFrame = function(index) { | |
| index -= listener_index; if(index >= 0 && index < listeners.length) { | |
| listeners[index] = function() {}; | |
| } | |
| }; | |
| } | |
| // setImmediate | |
| if(!window.setImmediate) { | |
| window.setImmediate = function(f) { return setTimeout(f, 0) }; | |
| window.cancelImmediate = clearTimeout; | |
| } | |
| }(); | |
| require.define('src/core/polyfill-dom-requestAnimationFrame.js'); | |
| //////////////////////////////////////// | |
| ///////////////////////////////////////////////////////////////// | |
| //// //// | |
| //// prerequirements of qSL //// | |
| //// //// | |
| ///////////////////////////////////////////////////////////////// | |
| //// //// | |
| //// Please note that I require querySelectorAll to work //// | |
| //// //// | |
| //// See http://github.com/termi/CSS_selector_engine/ //// | |
| //// for a polyfill for older browsers //// | |
| //// //// | |
| ///////////////////////////////////////////////////////////////// | |
| // TODO: improve event streams | |
| // - look for a few optimizations ideas in gecko/webkit | |
| // - use arrays in CompositeEventStream to avoid nested debouncings | |
| module.exports = (function(window, document) { "use strict"; | |
| /// | |
| /// event stream implementation | |
| /// please note this is required to 'live update' the qSA requests | |
| /// | |
| function EventStream(connect, disconnect, reconnect) { | |
| var self=this; | |
| // validate arguments | |
| if(!disconnect) disconnect=function(){}; | |
| if(!reconnect) reconnect=connect; | |
| // high-level states | |
| var isConnected=false; | |
| var isDisconnected=false; | |
| var shouldDisconnect=false; | |
| // global variables | |
| var callback=null; | |
| var yieldEvent = function() { | |
| // call the callback function, and pend disposal | |
| shouldDisconnect=true; | |
| try { callback && callback(self); } catch(ex) { setImmediate(function() { throw ex; }); } | |
| // if no action was taken, dispose | |
| if(shouldDisconnect) { dispose(); } | |
| } | |
| // export the interface | |
| var schedule = this.schedule = function(newCallback) { | |
| // do not allow to schedule on disconnected event streams | |
| if(isDisconnected) { throw new Error("Cannot schedule on a disconnected event stream"); } | |
| // do not allow to schedule on already scheduled event streams | |
| if(isConnected && !shouldDisconnect) { throw new Error("Cannot schedule on an already-scheduled event stream"); } | |
| // schedule the new callback | |
| callback=newCallback; shouldDisconnect=false; | |
| // reconnect to the stream | |
| if(isConnected) { | |
| reconnect(yieldEvent); | |
| } else { | |
| connect(yieldEvent); | |
| isConnected=true; | |
| } | |
| } | |
| var dispose = this.dispose = function() { | |
| // do not allow to dispose non-connected streams | |
| if(isConnected) { | |
| // disconnect & save resources | |
| disconnect(); | |
| self=null; yieldEvent=null; callback=null; | |
| isConnected=false; isDisconnected=true; shouldDisconnect=false; | |
| } | |
| } | |
| } | |
| /// | |
| /// call a function every frame | |
| /// | |
| function AnimationFrameEventStream(options) { | |
| // flag that says whether the observer is still needed or not | |
| var rid = 0; | |
| // start the event stream | |
| EventStream.call( | |
| this, | |
| function connect(yieldEvent) { rid = requestAnimationFrame(yieldEvent); }, | |
| function disconnect() { cancelAnimationFrame(rid); } | |
| ); | |
| } | |
| /// | |
| /// call a function every timeout | |
| /// | |
| function TimeoutEventStream(options) { | |
| // flag that says whether the observer is still needed or not | |
| var rid = 0; var timeout=(typeof(options)=="number") ? (+options) : ("timeout" in options ? +options.timeout : 333); | |
| // start the event stream | |
| EventStream.call( | |
| this, | |
| function connect(yieldEvent) { rid = setTimeout(yieldEvent, timeout); }, | |
| function disconnect() { clearTimeout(rid); } | |
| ); | |
| } | |
| /// | |
| /// call a function every time the mouse moves | |
| /// | |
| function MouseEventStream() { | |
| var self=this; var pointermove = (("PointerEvent" in window) ? "pointermove" : (("MSPointerEvent" in window) ? "MSPointerMove" : "mousemove")); | |
| // flag that says whether the event is still observed or not | |
| var scheduled = false; var interval=0; | |
| // handle the synchronous nature of mutation events | |
| var yieldEvent=null; | |
| var yieldEventDelayed = function() { | |
| if(scheduled) return; | |
| window.removeEventListener(pointermove, yieldEventDelayed, true); | |
| scheduled = requestAnimationFrame(yieldEvent); | |
| } | |
| // start the event stream | |
| EventStream.call( | |
| this, | |
| function connect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; | |
| window.addEventListener(pointermove, yieldEventDelayed, true); | |
| }, | |
| function disconnect() { | |
| window.removeEventListener(pointermove, yieldEventDelayed, true); | |
| cancelAnimationFrame(scheduled); yieldEventDelayed=null; yieldEvent=null; scheduled=false; | |
| }, | |
| function reconnect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; scheduled=false; | |
| window.addEventListener(pointermove, yieldEventDelayed, true); | |
| } | |
| ); | |
| } | |
| /// | |
| /// call a function every time the mouse is clicked/unclicked | |
| /// | |
| function MouseButtonEventStream() { | |
| var self=this; | |
| var pointerup = (("PointerEvent" in window) ? "pointerup" : (("MSPointerEvent" in window) ? "MSPointerUp" : "mouseup")); | |
| var pointerdown = (("PointerEvent" in window) ? "pointerdown" : (("MSPointerEvent" in window) ? "MSPointerDown" : "mousedown")); | |
| // flag that says whether the event is still observed or not | |
| var scheduled = false; var interval=0; | |
| // handle the synchronous nature of mutation events | |
| var yieldEvent=null; | |
| var yieldEventDelayed = function() { | |
| if(scheduled) return; | |
| window.removeEventListener(pointerup, yieldEventDelayed, true); | |
| window.removeEventListener(pointerdown, yieldEventDelayed, true); | |
| scheduled = requestAnimationFrame(yieldEvent); | |
| } | |
| // start the event stream | |
| EventStream.call( | |
| this, | |
| function connect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; | |
| window.addEventListener(pointerup, yieldEventDelayed, true); | |
| window.addEventListener(pointerdown, yieldEventDelayed, true); | |
| }, | |
| function disconnect() { | |
| window.removeEventListener(pointerup, yieldEventDelayed, true); | |
| window.removeEventListener(pointerdown, yieldEventDelayed, true); | |
| cancelAnimationFrame(scheduled); yieldEventDelayed=null; yieldEvent=null; scheduled=false; | |
| }, | |
| function reconnect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; scheduled=false; | |
| window.addEventListener(pointerup, yieldEventDelayed, true); | |
| window.addEventListener(pointerdown, yieldEventDelayed, true); | |
| } | |
| ); | |
| } | |
| /// | |
| /// call a function whenever the DOM is modified | |
| /// | |
| var DOMUpdateEventStream; | |
| if("MutationObserver" in window) { | |
| DOMUpdateEventStream = function DOMUpdateEventStream(options) { | |
| // configuration of the observer | |
| if(options) { | |
| var target = "target" in options ? options.target : document.documentElement; | |
| var config = { | |
| subtree: "subtree" in options ? !!options.subtree : true, | |
| attributes: "attributes" in options ? !!options.attributes : true, | |
| childList: "childList" in options ? !!options.childList : true, | |
| characterData: "characterData" in options ? !!options.characterData : false | |
| }; | |
| } else { | |
| var target = document.documentElement; | |
| var config = { | |
| subtree: true, | |
| attributes: true, | |
| childList: true, | |
| characterData: false | |
| }; | |
| } | |
| // start the event stream | |
| var observer = null; | |
| EventStream.call( | |
| this, | |
| function connect(yieldEvent) { if(config) { observer=new MutationObserver(yieldEvent); observer.observe(target,config); target=null; config=null; } }, | |
| function disconnect() { observer && observer.disconnect(); observer=null; }, | |
| function reconnect() { observer.takeRecords(); } | |
| ); | |
| } | |
| } else if("MutationEvent" in window) { | |
| DOMUpdateEventStream = function DOMUpdateEventStream(options) { | |
| var self=this; | |
| // flag that says whether the event is still observed or not | |
| var scheduled = false; | |
| // configuration of the observer | |
| if(options) { | |
| var target = "target" in options ? options.target : document.documentElement; | |
| } else { | |
| var target = document.documentElement; | |
| } | |
| // handle the synchronous nature of mutation events | |
| var yieldEvent=null; | |
| var yieldEventDelayed = function() { | |
| if(scheduled || !yieldEventDelayed) return; | |
| document.removeEventListener("DOMContentLoaded", yieldEventDelayed, false); | |
| document.removeEventListener("DOMContentLoaded", yieldEventDelayed, false); | |
| target.removeEventListener("DOMSubtreeModified", yieldEventDelayed, false); | |
| scheduled = requestAnimationFrame(yieldEvent); | |
| } | |
| // start the event stream | |
| EventStream.call( | |
| this, | |
| function connect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; | |
| document.addEventListener("DOMContentLoaded", yieldEventDelayed, false); | |
| target.addEventListener("DOMSubtreeModified", yieldEventDelayed, false); | |
| }, | |
| function disconnect() { | |
| document.removeEventListener("DOMContentLoaded", yieldEventDelayed, false); | |
| target.removeEventListener("DOMSubtreeModified", yieldEventDelayed, false); | |
| cancelAnimationFrame(scheduled); yieldEventDelayed=null; yieldEvent=null; scheduled=false; | |
| }, | |
| function reconnect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; scheduled=false; | |
| target.addEventListener("DOMSubtreeModified", yieldEventDelayed, false); | |
| } | |
| ); | |
| } | |
| } else { | |
| DOMUpdateEventStream = AnimationFrameEventStream; | |
| } | |
| /// | |
| /// call a function every time the focus shifts | |
| /// | |
| function FocusEventStream() { | |
| var self=this; | |
| // handle the filtering nature of focus events | |
| var yieldEvent=null; var previousActiveElement=null; var previousHasFocus=false; var rid=0; | |
| var yieldEventDelayed = function() { | |
| // if the focus didn't change | |
| if(previousActiveElement==document.activeElement && previousHasFocus==document.hasFocus()) { | |
| // then do not generate an event | |
| setTimeout(yieldEventDelayed, 333); // focus that didn't move is expected to stay | |
| } else { | |
| // else, generate one & save config | |
| previousActiveElement=document.activeElement; | |
| previousHasFocus=document.hasFocus(); | |
| yieldEvent(); | |
| } | |
| } | |
| // start the event stream | |
| EventStream.call( | |
| this, | |
| function connect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; | |
| rid=setTimeout(yieldEventDelayed, 500); // let the document load | |
| }, | |
| function disconnect() { | |
| clearTimeout(rid); yieldEventDelayed=null; yieldEvent=null; rid=0; | |
| }, | |
| function reconnect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; | |
| rid=setTimeout(yieldEventDelayed, 100); // focus by tab navigation moves fast | |
| } | |
| ); | |
| } | |
| /// | |
| /// composite event stream | |
| /// because sometimes you need more than one event source | |
| /// | |
| function CompositeEventStream(stream1, stream2) { | |
| var self=this; | |
| // fields | |
| var yieldEvent=null; var s1=false, s2=false; | |
| var yieldEventWrapper=function(s) { | |
| if(s==stream1) s1=true; | |
| if(s==stream2) s2=true; | |
| if(s1&&s2) return; | |
| yieldEvent(self); | |
| } | |
| // start the event stream | |
| EventStream.call( | |
| this, | |
| function connect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; | |
| stream1.schedule(yieldEventWrapper); | |
| stream2.schedule(yieldEventWrapper); | |
| }, | |
| function disconnect() { | |
| stream1.dispose(); | |
| stream2.dispose(); | |
| }, | |
| function reconnect(newYieldEvent) { | |
| yieldEvent=newYieldEvent; | |
| s1 && stream1.schedule(yieldEventWrapper); | |
| s2 && stream2.schedule(yieldEventWrapper); | |
| s1 = s2 = false; | |
| } | |
| ); | |
| } | |
| return { | |
| EventStream: EventStream, | |
| AnimationFrameEventStream: AnimationFrameEventStream, | |
| TimeoutEventStream: TimeoutEventStream, | |
| MouseEventStream: MouseEventStream, | |
| MouseButtonEventStream: MouseButtonEventStream, | |
| DOMUpdateEventStream: DOMUpdateEventStream, | |
| FocusEventStream: FocusEventStream, | |
| CompositeEventStream: CompositeEventStream | |
| }; | |
| })(window, document); | |
| require.define('src/core/dom-experimental-event-streams.js'); | |
| //////////////////////////////////////// | |
| ///////////////////////////////////////////////////////////////// | |
| //// //// | |
| //// Implementation of qSL //// | |
| //// //// | |
| ///////////////////////////////////////////////////////////////// | |
| //// //// | |
| //// Please note that I require querySelectorAll to work //// | |
| //// //// | |
| //// See http://github.com/termi/CSS_selector_engine/ //// | |
| //// for a polyfill for older browsers //// | |
| //// //// | |
| ///////////////////////////////////////////////////////////////// | |
| module.exports = (function(window, document) { "use strict"; | |
| // import dependencies | |
| var eventStreams = require('src/core/dom-experimental-event-streams.js'), | |
| DOMUpdateEventStream = eventStreams.DOMUpdateEventStream, | |
| AnimationFrameEventStream = eventStreams.AnimationFrameEventStream, | |
| CompositeEventStream = eventStreams.CompositeEventStream, | |
| FocusEventStream = eventStreams.FocusEventStream, | |
| MouseButtonEventStream = eventStreams.MouseButtonEventStream, | |
| TimeoutEventStream = eventStreams.TimeoutEventStream, | |
| MouseEventStream = eventStreams.MouseEventStream; | |
| /// | |
| /// the live querySelectorAll implementation | |
| /// | |
| function querySelectorLive(selector, handler, root) { | |
| // restrict the selector coverage to some part of the DOM only | |
| var root = root || document; | |
| // TODO: make use of "mutatedAncestorElement" to update only elements inside the mutated zone | |
| var currentElms = []; | |
| var loop = function loop(eventStream) { | |
| // schedule next run | |
| eventStream.schedule(loop); | |
| // update elements matching the selector | |
| var newElms = []; | |
| var oldElms = currentElms.slice(0); | |
| var temps = root.querySelectorAll(selector); | |
| for(var i=newElms.length=temps.length; i;) { newElms.push(temps[--i]); } | |
| currentElms = newElms.slice(0); temps=null; | |
| // first let's clear all elements that have been removed from the document | |
| oldElms = oldElms.filter(function(e) { | |
| // check whether the current element is still there | |
| var isStillInDocument = ( | |
| e===document.documentElement | |
| || document.documentElement.contains(e) | |
| ); | |
| if(isStillInDocument) { | |
| // NEED_COMPARE: we will compare this element to the new list | |
| return true; | |
| } else { | |
| // DELETE: raise onremoved, pop old elements | |
| try { handler.onremoved && handler.onremoved(e); } catch(ex) { setImmediate(function() {throw ex})} | |
| return false; | |
| } | |
| }); | |
| // now pop and match until both lists are exhausted | |
| // (we use the fact the returned elements are in document order) | |
| var el1 = oldElms.pop(); | |
| var el2 = newElms.pop(); | |
| while(el1 || el2) { | |
| if(el1===el2) { | |
| // MATCH: pop both elements | |
| el1 = oldElms.pop(); | |
| el2 = newElms.pop(); | |
| } else if (el2 && /*el1 is after el2*/(!el1||(el2.compareDocumentPosition(el1) & (1|2|8|32))===0)) { | |
| // INSERT: raise onadded, pop new elements | |
| try { handler.onadded && handler.onadded(el2); } catch(ex) { setImmediate(function() {throw ex})} | |
| el2 = newElms.pop(); | |
| } else { | |
| // DELETE: raise onremoved, pop old elements | |
| try { handler.onremoved && handler.onremoved(el1); } catch(ex) { setImmediate(function() {throw ex})} | |
| el1 = oldElms.pop(); | |
| } | |
| } | |
| }; | |
| // use the event stream that best matches our needs | |
| var simpleSelector = selector.replace(/:(dir|lang|root|empty|blank|nth-child|nth-last-child|first-child|last-child|only-child|nth-of-type|nth-last-of-child|fist-of-type|last-of-type|only-of-type|not|matches|default)\b/gi,'') | |
| var eventStream; if(simpleSelector.indexOf(':') == -1) { | |
| // static stuff only | |
| eventStream = new DOMUpdateEventStream({target:root}); | |
| } else { | |
| // dynamic stuff too | |
| eventStream = new DOMUpdateEventStream({target:root}); | |
| if(DOMUpdateEventStream != AnimationFrameEventStream) { | |
| // detect the presence of focus-related pseudo-classes | |
| var reg = /:(focus|active)\b/gi; | |
| if(reg.test(simpleSelector)) { | |
| // mouse events should be listened | |
| eventStream = new CompositeEventStream( | |
| new FocusEventStream(), | |
| eventStream | |
| ); | |
| // simplify simpleSelector | |
| var reg = /:(focus)\b/gi; | |
| simpleSelector = simpleSelector.replace(reg, ''); // :active has other hooks | |
| } | |
| // detect the presence of mouse-button-related pseudo-classes | |
| var reg = /:(active)\b/gi; | |
| if(reg.test(simpleSelector)) { | |
| // mouse events should be listened | |
| eventStream = new CompositeEventStream( | |
| new MouseButtonEventStream(), | |
| eventStream | |
| ); | |
| // simplify simpleSelector | |
| simpleSelector = simpleSelector.replace(reg, ''); | |
| } | |
| // detect the presence of user input pseudo-classes | |
| var reg = /:(target|checked|indeterminate|valid|invalid|in-range|out-of-range|user-error)\b/gi; | |
| if(reg.test(simpleSelector)) { | |
| // slowly dynamic stuff do happen | |
| eventStream = new CompositeEventStream( | |
| new TimeoutEventStream(250), | |
| eventStream | |
| ); | |
| // simplify simpleSelector | |
| simpleSelector = simpleSelector.replace(reg, ''); | |
| var reg = /:(any-link|link|visited|local-link|enabled|disabled|read-only|read-write|required|optional)\b/gi; | |
| // simplify simpleSelector | |
| simpleSelector = simpleSelector.replace(reg, ''); | |
| } | |
| // detect the presence of nearly-static pseudo-classes | |
| var reg = /:(any-link|link|visited|local-link|enabled|disabled|read-only|read-write|required|optional)\b/gi; | |
| if(reg.test(simpleSelector)) { | |
| // nearly static stuff do happen | |
| eventStream = new CompositeEventStream( | |
| new TimeoutEventStream(333), | |
| eventStream | |
| ); | |
| // simplify simpleSelector | |
| simpleSelector = simpleSelector.replace(reg, ''); | |
| } | |
| // detect the presence of mouse-related pseudo-classes | |
| var reg = /:(hover)\b/gi; | |
| if(reg.test(simpleSelector)) { | |
| // mouse events should be listened | |
| eventStream = new CompositeEventStream( | |
| new MouseEventStream(), | |
| eventStream | |
| ); | |
| // simplify simpleSelector | |
| simpleSelector = simpleSelector.replace(reg, ''); | |
| } | |
| // detect the presence of unknown pseudo-classes | |
| if(simpleSelector.indexOf(':') !== -1) { | |
| // other stuff do happen, too (let's give up on events) | |
| eventStream = new AnimationFrameEventStream(); | |
| } | |
| } | |
| } | |
| // start handling changes | |
| loop(eventStream); | |
| } | |
| return querySelectorLive; | |
| })(window, document); | |
| require.define('src/core/dom-query-selector-live.js'); | |
| //////////////////////////////////////// | |
| // TODO: comment about the 'no_auto_stylesheet_detection' flag? | |
| module.exports = (function(window, document) { "use strict"; | |
| // import dependencies | |
| require('src/core/polyfill-dom-console.js'); | |
| require('src/core/polyfill-dom-requestAnimationFrame.js'); | |
| var cssSyntax = require('src/core/css-syntax.js'); | |
| var domEvents = require('src/core/dom-events.js'); | |
| var querySelectorLive = require('src/core/dom-query-selector-live.js'); | |
| // define the module | |
| var cssCascade = { | |
| // | |
| // returns the priority of a unique selector (NO COMMA!) | |
| // { the return value is an integer, with the same formula as webkit } | |
| // | |
| computeSelectorPriorityOf: function computeSelectorPriorityOf(selector) { | |
| if(typeof selector == "string") selector = cssSyntax.parse(selector.trim()+"{}").value[0].selector; | |
| var numberOfIDs = 0; | |
| var numberOfClasses = 0; | |
| var numberOfTags = 0; | |
| // TODO: improve this parser, or find one on the web | |
| for(var i = 0; i < selector.length; i++) { | |
| if(selector[i] instanceof cssSyntax.IdentifierToken) { | |
| numberOfTags++; | |
| } else if(selector[i] instanceof cssSyntax.DelimToken) { | |
| if(selector[i].value==".") { | |
| numberOfClasses++; i++; | |
| } | |
| } else if(selector[i] instanceof cssSyntax.ColonToken) { | |
| if(selector[++i] instanceof cssSyntax.ColonToken) { | |
| numberOfTags++; i++; | |
| } else if((selector[i] instanceof cssSyntax.Func) && (/^(not|matches)$/i).test(selector[i].name)) { | |
| var nestedPriority = this.computeSelectorPriorityOf(selector[i].value); | |
| numberOfTags += nestedPriority % 256; nestedPriority /= 256; | |
| numberOfClasses += nestedPriority % 256; nestedPriority /= 256; | |
| numberOfIDs += nestedPriority; | |
| } else { | |
| numberOfClasses++; | |
| } | |
| } else if(selector[i] instanceof cssSyntax.SimpleBlock) { | |
| if(selector[i].name=="[") { | |
| numberOfClasses++; | |
| } | |
| } else if(selector[i] instanceof cssSyntax.HashToken) { | |
| numberOfIDs++; | |
| } else { | |
| // TODO: stop ignoring unknown symbols? | |
| } | |
| } | |
| if(numberOfIDs>255) numberOfIds=255; | |
| if(numberOfClasses>255) numberOfClasses=255; | |
| if(numberOfTags>255) numberOfTags=255; | |
| return ((numberOfIDs*256)+numberOfClasses)*256+numberOfTags; | |
| }, | |
| // | |
| // returns an array of the css rules matching an element | |
| // | |
| findAllMatchingRules: function findAllMatchingRules(element) { | |
| return this.findAllMatchingRulesWithPseudo(element); | |
| }, | |
| // | |
| // returns an array of the css rules matching a pseudo-element | |
| // | |
| findAllMatchingRulesWithPseudo: function findAllMatchingRules(element,pseudo) { | |
| pseudo = pseudo ? (''+pseudo).toLowerCase() : pseudo; | |
| // let's look for new results if needed... | |
| var results = []; | |
| // walk the whole stylesheet... | |
| var visit = function(rules) { | |
| try { | |
| for(var r = rules.length; r--; ) { | |
| var rule = rules[r]; | |
| // media queries hook | |
| if(rule.disabled) continue; | |
| if(rule instanceof cssSyntax.StyleRule) { | |
| // consider each selector independently | |
| var subrules = rule.subRules || cssCascade.splitRule(rule); | |
| for(var sr = subrules.length; sr--; ) { | |
| var selector = subrules[sr].selector.toCSSString().replace(/ *(\/\*\*\/| ) */g,' ').trim(); | |
| if(pseudo) { | |
| // WE ONLY ACCEPT SELECTORS ENDING WITH THE PSEUDO | |
| var selectorLow = selector.toLowerCase(); | |
| var newLength = selector.length-pseudo.length-1; | |
| if(newLength<=0) continue; | |
| if(selectorLow.lastIndexOf('::'+pseudo)==newLength-1) { | |
| selector = selector.substr(0,newLength-1); | |
| } else if(selectorLow.lastIndexOf(':'+pseudo)==newLength) { | |
| selector = selector.substr(0,newLength); | |
| } else { | |
| continue; | |
| } | |
| // fix selectors like "#element > :first-child ~ ::before" | |
| if(selector.trim().length == 0) { selector = '*' } | |
| else if(selector[selector.length-1] == ' ') { selector += '*' } | |
| else if(selector[selector.length-1] == '+') { selector += '*' } | |
| else if(selector[selector.length-1] == '>') { selector += '*' } | |
| else if(selector[selector.length-1] == '~') { selector += '*' } | |
| } | |
| // look if the selector matches | |
| var isMatching = false; | |
| try { | |
| if(element.matches) isMatching=element.matches(selector) | |
| else if(element.matchesSelector) isMatching=element.matchesSelector(selector) | |
| else if(element.oMatchesSelector) isMatching=element.oMatchesSelector(selector) | |
| else if(element.msMatchesSelector) isMatching=element.msMatchesSelector(selector) | |
| else if(element.mozMatchesSelector) isMatching=element.mozMatchesSelector(selector) | |
| else if(element.webkitMatchesSelector) isMatching=element.webkitMatchesSelector(selector) | |
| else { throw new Error("no element.matches?") } | |
| } catch(ex) { debugger; setImmediate(function() { throw ex; }) } | |
| // if yes, add it to the list of matched selectors | |
| if(isMatching) { results.push(subrules[sr]); } | |
| } | |
| } else if(rule instanceof cssSyntax.AtRule && rule.name=="media") { | |
| // visit them | |
| visit(rule.toStylesheet().value); | |
| } | |
| } | |
| } catch (ex) { | |
| setImmediate(function() { throw ex; }); | |
| } | |
| } | |
| for(var s=cssCascade.stylesheets.length; s--; ) { | |
| var rules = cssCascade.stylesheets[s]; | |
| visit(rules); | |
| } | |
| return results; | |
| }, | |
| // | |
| // a list of all properties supported by the current browser | |
| // | |
| allCSSProperties: null, | |
| getAllCSSProperties: function getAllCSSProperties() { | |
| if(this.allCSSProperties) return this.allCSSProperties; | |
| // get all claimed properties | |
| var s = getComputedStyle(document.documentElement); var ps = new Array(s.length); | |
| for(var i=s.length; i--; ) { | |
| ps[i] = s[i]; | |
| } | |
| // FIX A BUG WHERE WEBKIT DOESN'T REPORT ALL PROPERTIES | |
| if(ps.indexOf('content')==-1) {ps.push('content');} | |
| if(ps.indexOf('counter-reset')==-1) { | |
| ps.push('counter-reset'); | |
| ps.push('counter-increment'); | |
| // FIX A BUG WHERE WEBKIT RETURNS SHIT FOR THE COMPUTED VALUE OF COUNTER-RESET | |
| cssCascade.computationUnsafeProperties['counter-reset']=true; | |
| } | |
| // save in a cache for faster access the next times | |
| return this.allCSSProperties = ps; | |
| }, | |
| // | |
| // those properties are not safe for computation->specified round-tripping | |
| // | |
| computationUnsafeProperties: { | |
| "bottom" : true, | |
| "direction" : true, | |
| "display" : true, | |
| "font-size" : true, | |
| "height" : true, | |
| "left" : true, | |
| "line-height" : true, | |
| "margin-left" : true, | |
| "margin-right" : true, | |
| "margin-bottom" : true, | |
| "margin-top" : true, | |
| "max-height" : true, | |
| "max-width" : true, | |
| "min-height" : true, | |
| "min-width" : true, | |
| "padding-left" : true, | |
| "padding-right" : true, | |
| "padding-bottom" : true, | |
| "padding-top" : true, | |
| "right" : true, | |
| "text-align" : true, | |
| "text-align-last" : true, | |
| "top" : true, | |
| "width" : true, | |
| __proto__ : null, | |
| }, | |
| // | |
| // a list of property we should inherit... | |
| // | |
| inheritingProperties: { | |
| "border-collapse" : true, | |
| "border-spacing" : true, | |
| "caption-side" : true, | |
| "color" : true, | |
| "cursor" : true, | |
| "direction" : true, | |
| "empty-cells" : true, | |
| "font-family" : true, | |
| "font-size" : true, | |
| "font-style" : true, | |
| "font-variant" : true, | |
| "font-weight" : true, | |
| "font" : true, | |
| "letter-spacing" : true, | |
| "line-height" : true, | |
| "list-style-image" : true, | |
| "list-style-position" : true, | |
| "list-style-type" : true, | |
| "list-style" : true, | |
| "orphans" : true, | |
| "quotes" : true, | |
| "text-align" : true, | |
| "text-indent" : true, | |
| "text-transform" : true, | |
| "visibility" : true, | |
| "white-space" : true, | |
| "widows" : true, | |
| "word-break" : true, | |
| "word-spacing" : true, | |
| "word-wrap" : true, | |
| __proto__ : null, | |
| }, | |
| // | |
| // returns the default style for a tag | |
| // | |
| defaultStylesForTag: Object.create ? Object.create(null) : {}, | |
| getDefaultStyleForTag: function getDefaultStyleForTag(tagName) { | |
| // get result from cache | |
| var result = this.defaultStylesForTag[tagName]; | |
| if(result) return result; | |
| // create dummy virtual element | |
| var element = document.createElement(tagName); | |
| var style = this.defaultStylesForTag[tagName] = getComputedStyle(element); | |
| if(style.display) return style; | |
| // webkit fix: insert the dummy element anywhere (head -> display:none) | |
| document.head.insertBefore(element, document.head.firstChild); | |
| return style; | |
| }, | |
| // | |
| // returns the specified style of an element. | |
| // REMARK: may or may not unwrap "inherit" and "initial" depending on implementation | |
| // REMARK: giving "matchedRules" as a parameter allow you to mutualize the "findAllMatching" rules calls | |
| // | |
| getSpecifiedStyle: function getSpecifiedStyle(element, cssPropertyName, matchedRules) { | |
| // hook for css regions | |
| var fragmentSource; | |
| if(fragmentSource=element.getAttribute('data-css-regions-fragment-of')) { | |
| fragmentSource = document.querySelector('[data-css-regions-fragment-source="'+fragmentSource+'"]'); | |
| if(fragmentSource) return cssCascade.getSpecifiedStyle(fragmentSource, cssPropertyName); | |
| } | |
| // give IE a thumbs up for this! | |
| if(element.currentStyle && !window.opera) { | |
| // ask IE to manage the style himself... | |
| var bestValue = element.myStyle[cssPropertyName] || element.currentStyle[cssPropertyName] || ''; | |
| // return a parsed representation of the value | |
| return cssSyntax.parseAListOfComponentValues(bestValue); | |
| } else { | |
| // TODO: support the "initial" and "inherit" things? | |
| // first, let's try inline style as it's fast and generally accurate | |
| // TODO: what if important rules override that? | |
| try { | |
| if(bestValue = element.style.getPropertyValue(cssPropertyName) || element.myStyle[cssPropertyName]) { | |
| return cssSyntax.parseAListOfComponentValues(bestValue); | |
| } | |
| } catch(ex) {} | |
| // find all relevant style rules | |
| var isBestImportant=false; var bestPriority = 0; var bestValue = new cssSyntax.TokenList(); | |
| var rules = matchedRules || ( | |
| cssPropertyName in cssCascade.monitoredProperties | |
| ? element.myMatchedRules || [] | |
| : cssCascade.findAllMatchingRules(element) | |
| ); | |
| var visit = function(rules) { | |
| for(var i=rules.length; i--; ) { | |
| // media queries hook | |
| if(rules[i].disabled) continue; | |
| // find a relevant declaration | |
| if(rules[i] instanceof cssSyntax.StyleRule) { | |
| var decls = rules[i].getDeclarations(); | |
| for(var j=decls.length-1; j>=0; j--) { | |
| if(decls[j].type=="DECLARATION") { | |
| if(decls[j].name==cssPropertyName) { | |
| // only works if selectors containing a "," are deduplicated | |
| var currentPriority = cssCascade.computeSelectorPriorityOf(rules[i].selector); | |
| if(isBestImportant) { | |
| // only an important declaration can beat another important declaration | |
| if(decls[j].important) { | |
| if(currentPriority >= bestPriority) { | |
| bestPriority = currentPriority; | |
| bestValue = decls[j].value; | |
| } | |
| } | |
| } else { | |
| // an important declaration beats any non-important declaration | |
| if(decls[j].important) { | |
| isBestImportant = true; | |
| bestPriority = currentPriority; | |
| bestValue = decls[j].value; | |
| } else { | |
| // the selector priority has to be higher otherwise | |
| if(currentPriority >= bestPriority) { | |
| bestPriority = currentPriority; | |
| bestValue = decls[j].value; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } else if((rules[i] instanceof cssSyntax.AtRule) && (rules[i].name=="media")) { | |
| // visit them | |
| visit(rules[i].toStylesheet()) | |
| } | |
| } | |
| } | |
| visit(rules); | |
| // return our best guess... | |
| return bestValue||null; | |
| } | |
| }, | |
| // | |
| // start monitoring a new stylesheet | |
| // (should usually not be used because stylesheets load automatically) | |
| // | |
| stylesheets: [], | |
| loadStyleSheet: function loadStyleSheet(cssText,i) { | |
| // load in order | |
| // parse the stylesheet content | |
| var rules = cssSyntax.parse(cssText).value; | |
| // add the stylesheet into the object model | |
| if(typeof(i)!=="undefined") { cssCascade.stylesheets[i]=rules; } | |
| else { i=cssCascade.stylesheets.push(rules);} | |
| // make sure to monitor the required rules | |
| cssCascade.startMonitoringStylesheet(rules) | |
| }, | |
| // | |
| // start monitoring a new stylesheet | |
| // (should usually not be used because stylesheets load automatically) | |
| // | |
| loadStyleSheetTag: function loadStyleSheetTag(stylesheet,i) { | |
| if(stylesheet.hasAttribute('data-css-polyfilled')) { | |
| return; | |
| } | |
| if(stylesheet.tagName=='LINK') { | |
| // oh, no, we have to download it... | |
| try { | |
| // dummy value in-between | |
| cssCascade.stylesheets[i] = new cssSyntax.TokenList(); | |
| // | |
| var xhr = new XMLHttpRequest(); xhr.href = stylesheet.href; | |
| xhr.open('GET',stylesheet.href,true); xhr.ruleIndex = i; | |
| xhr.onreadystatechange = function() { | |
| if(this.readyState==4) { | |
| // status 0 is a webkit bug for local files | |
| if(this.status==200||this.status==0) { | |
| cssCascade.loadStyleSheet(this.responseText,this.ruleIndex) | |
| } else { | |
| cssConsole.log("css-cascade polyfill failled to load: " + this.href); | |
| } | |
| } | |
| }; | |
| xhr.send(); | |
| } catch(ex) { | |
| cssConsole.log("css-cascade polyfill failled to load: " + stylesheet.href); | |
| } | |
| } else { | |
| // oh, cool, we just have to parse the content! | |
| cssCascade.loadStyleSheet(stylesheet.textContent,i); | |
| } | |
| // mark the stylesheet as ok | |
| stylesheet.setAttribute('data-css-polyfilled',true); | |
| }, | |
| // | |
| // calling this function will load all currently existing stylesheets in the document | |
| // (should usually not be used because stylesheets load automatically) | |
| // | |
| selectorForStylesheets: "style:not([data-no-css-polyfill]):not([data-css-polyfilled]), link[rel=stylesheet]:not([data-no-css-polyfill]):not([data-css-polyfilled])", | |
| loadAllStyleSheets: function loadAllStyleSheets() { | |
| // for all stylesheets in the <head> tag... | |
| var head = document.head || document.documentElement; | |
| var stylesheets = head.querySelectorAll(cssCascade.selectorForStylesheets); | |
| var intialLength = this.stylesheets.length; | |
| this.stylesheets.length += stylesheets.length | |
| // for all of them... | |
| for(var i = stylesheets.length; i--;) { | |
| // | |
| // load the stylesheet | |
| // | |
| var stylesheet = stylesheets[i]; | |
| cssCascade.loadStyleSheetTag(stylesheet,intialLength+i) | |
| } | |
| }, | |
| // | |
| // this is where we store event handlers for monitored properties | |
| // | |
| monitoredProperties: Object.create ? Object.create(null) : {}, | |
| monitoredPropertiesHandler: { | |
| onupdate: function(element, rule) { | |
| // we need to find all regexps that matches | |
| var mps = cssCascade.monitoredProperties; | |
| var decls = rule.getDeclarations(); | |
| for(var j=decls.length-1; j>=0; j--) { | |
| if(decls[j].type=="DECLARATION") { | |
| if(decls[j].name in mps) { | |
| // call all handlers waiting for this | |
| var hs = mps[decls[j].name]; | |
| for(var hi=hs.length; hi--;) { | |
| hs[hi].onupdate(element,rule); | |
| }; | |
| // don't call twice | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| }, | |
| // | |
| // add an handler to some properties (aka fire when their value *MAY* be affected) | |
| // REMARK: because this event does not promise the value changed, you may want to figure it out before relayouting | |
| // | |
| startMonitoringProperties: function startMonitoringProperties(properties, handler) { | |
| for(var i=properties.length; i--; ) { | |
| var property = properties[i]; | |
| var handlers = ( | |
| cssCascade.monitoredProperties[property] | |
| || (cssCascade.monitoredProperties[property] = []) | |
| ); | |
| handlers.push(handler) | |
| } | |
| for(var s=0; s<cssCascade.stylesheets.length; s++) { | |
| var currentStylesheet = cssCascade.stylesheets[s]; | |
| cssCascade.startMonitoringStylesheet(currentStylesheet); | |
| } | |
| }, | |
| // | |
| // calling this function will detect monitored rules in the stylesheet | |
| // (should usually not be used because stylesheets load automatically) | |
| // | |
| startMonitoringStylesheet: function startMonitoringStylesheet(rules) { | |
| for(var i=0; i<rules.length; i++) { | |
| // only consider style rules | |
| if(rules[i] instanceof cssSyntax.StyleRule) { | |
| // try to see if the current rule is worth monitoring | |
| if(rules[i].isMonitored) continue; | |
| // for that, let's see if we can find a declaration we should watch | |
| var decls = rules[i].getDeclarations(); | |
| for(var j=decls.length-1; j>=0; j--) { | |
| if(decls[j].type=="DECLARATION") { | |
| if(decls[j].name in cssCascade.monitoredProperties) { | |
| // if we found some, start monitoring | |
| cssCascade.startMonitoringRule(rules[i]); | |
| break; | |
| } | |
| } | |
| } | |
| } else if(rules[i] instanceof cssSyntax.AtRule) { | |
| // handle @media | |
| if(rules[i].name == "media" && window.matchMedia) { | |
| cssCascade.startMonitoringMedia(rules[i]); | |
| } | |
| } | |
| } | |
| }, | |
| // | |
| // calling this function will detect media query updates and fire events accordingly | |
| // (should usually not be used because stylesheets load automatically) | |
| // | |
| startMonitoringMedia: function startMonitoringMedia(atrule) { | |
| try { | |
| var media = window.matchMedia(atrule.prelude.toCSSString()); | |
| // update all the rules when needed | |
| var rules = atrule.toStylesheet().value; | |
| cssCascade.updateMedia(rules, !media.matches, false); | |
| media.addListener( | |
| function(newMedia) { cssCascade.updateMedia(rules, !newMedia.matches, true); } | |
| ); | |
| // it seems I like taking risks... | |
| cssCascade.startMonitoringStylesheet(rules); | |
| } catch(ex) { | |
| setImmediate(function() { throw ex; }) | |
| } | |
| }, | |
| // | |
| // define what happens when a media query status changes | |
| // | |
| updateMedia: function(rules,disabled,update) { | |
| for(var i=rules.length; i--; ) { | |
| rules[i].disabled = disabled; | |
| // TODO: should probably get handled by a setter on the rule... | |
| var sr = rules[i].subRules; | |
| if(sr) { | |
| for(var j=sr.length; j--; ) { | |
| sr[j].disabled = disabled; | |
| } | |
| } | |
| } | |
| // in case of update, all elements matching the selector went potentially updated... | |
| if(update) { | |
| for(var i=rules.length; i--; ) { | |
| var els = document.querySelectorAll(rules[i].selector.toCSSString()); | |
| for(var j=els.length; j--; ) { | |
| cssCascade.monitoredPropertiesHandler.onupdate(els[j],rules[i]); | |
| } | |
| } | |
| } | |
| }, | |
| // | |
| // splits a rule if it has multiple selectors | |
| // | |
| splitRule: function splitRule(rule) { | |
| // create an array for all the subrules | |
| var rules = []; | |
| // fill the array | |
| var currentRule = new cssSyntax.StyleRule(); currentRule.disabled=rule.disabled; | |
| for(var i=0; i<rule.selector.length; i++) { | |
| if(rule.selector[i] instanceof cssSyntax.DelimToken && rule.selector[i].value==",") { | |
| currentRule.value = rule.value; rules.push(currentRule); | |
| currentRule = new cssSyntax.StyleRule(); currentRule.disabled=rule.disabled; | |
| } else { | |
| currentRule.selector.push(rule.selector[i]) | |
| } | |
| } | |
| currentRule.value = rule.value; rules.push(currentRule); | |
| // save the result of the split as subrules | |
| return rule.subRules = rules; | |
| }, | |
| // | |
| // ask the css-selector implementation to notify changes for the rules | |
| // | |
| startMonitoringRule: function startMonitoringRule(rule) { | |
| // avoid monitoring rules twice | |
| if(!rule.isMonitored) { rule.isMonitored=true } else { return; } | |
| // split the rule if it has multiple selectors | |
| var rules = rule.subRules || cssCascade.splitRule(rule); | |
| // monitor the rules | |
| for(var i=0; i<rules.length; i++) { | |
| rule = rules[i]; | |
| querySelectorLive(rule.selector.toCSSString(), { | |
| onadded: function(e) { | |
| // add the rule to the matching list of this element | |
| (e.myMatchedRules = e.myMatchedRules || []).unshift(rule); // TODO: does not respect priority order | |
| // generate an update event | |
| cssCascade.monitoredPropertiesHandler.onupdate(e, rule); | |
| }, | |
| onremoved: function(e) { | |
| // remove the rule from the matching list of this element | |
| if(e.myMatchedRules) e.myMatchedRules.splice(e.myMatchedRules.indexOf(rule), 1); | |
| // generate an update event | |
| cssCascade.monitoredPropertiesHandler.onupdate(e, rule); | |
| } | |
| }); | |
| } | |
| }, | |
| // | |
| // converts a css property name to a javascript name | |
| // | |
| toCamelCase: function toCamelCase(variable) { | |
| return variable.replace( | |
| /-([a-z])/g, | |
| function(str,letter) { | |
| return letter.toUpperCase(); | |
| } | |
| ); | |
| }, | |
| // | |
| // add some magic code to support properties on the style interface | |
| // | |
| polyfillStyleInterface: function(cssPropertyName) { | |
| var prop = { | |
| get: function() { | |
| // check we know which element we work on | |
| try { if(!this.parentElement) throw new Error("Please use the anHTMLElement.myStyle property to get polyfilled properties") } | |
| catch(ex) { setImmediate(function() { throw ex; }); return ''; } | |
| try { | |
| // non-computed style: return the local style of the element | |
| this.clip = (this.clip===undefined?'':this.clip); | |
| return this.parentElement.getAttribute('data-style-'+cssPropertyName); | |
| } catch (ex) { | |
| // computed style: return the specified style of the element | |
| var value = cssCascade.getSpecifiedStyle(this.parentElement, cssPropertyName, undefined, true); | |
| return value && value.length>0 ? value.toCSSString() : ''; | |
| } | |
| }, | |
| set: function(v) { | |
| // check that the style is writable | |
| this.clip = (this.clip===undefined?'':this.clip); | |
| // check we know which element we work on | |
| try { if(!this.parentElement) throw new Error("Please use the anHTMLElement.myStyle property to set polyfilled properties") } | |
| catch(ex) { setImmediate(function() { throw ex; }); return; } | |
| // modify the local style of the element | |
| if(this.parentElement.getAttribute('data-style-'+cssPropertyName) != v) { | |
| this.parentElement.setAttribute('data-style-'+cssPropertyName,v); | |
| } | |
| } | |
| }; | |
| var styleProtos = []; | |
| try { styleProtos.push(Object.getPrototypeOf(document.documentElement.style) || CSSStyleDeclaration); } catch (ex) {} | |
| //try { styleProtos.push(Object.getPrototypeOf(getComputedStyle(document.documentElement))); } catch (ex) {} | |
| //try { styleProtos.push(Object.getPrototypeOf(document.documentElement.currentStyle)); } catch (ex) {} | |
| //try { styleProtos.push(Object.getPrototypeOf(document.documentElement.runtimeStyle)); } catch (ex) {} | |
| //try { styleProtos.push(Object.getPrototypeOf(document.documentElement.specifiedStyle)); } catch (ex) {} | |
| //try { styleProtos.push(Object.getPrototypeOf(document.documentElement.cascadedStyle)); } catch (ex) {} | |
| //try { styleProtos.push(Object.getPrototypeOf(document.documentElement.usedStyle)); } catch (ex) {} | |
| for(var i = styleProtos.length; i--;) { | |
| var styleProto = styleProtos[i]; | |
| Object.defineProperty(styleProto,cssPropertyName,prop); | |
| Object.defineProperty(styleProto,cssCascade.toCamelCase(cssPropertyName),prop); | |
| } | |
| cssCascade.startMonitoringRule(cssSyntax.parse('[style*="'+cssPropertyName+'"]{'+cssPropertyName+':attr(style)}').value[0]); | |
| cssCascade.startMonitoringRule(cssSyntax.parse('[data-style-'+cssPropertyName+']{'+cssPropertyName+':attr(style)}').value[0]); | |
| // add to the list of polyfilled properties... | |
| cssCascade.getAllCSSProperties().push(cssPropertyName); | |
| cssCascade.computationUnsafeProperties[cssPropertyName] = true; | |
| } | |
| }; | |
| // | |
| // polyfill for browsers not support CSSStyleDeclaration.parentElement (all of them right now) | |
| // | |
| domEvents.EventTarget.implementsIn(cssCascade); | |
| Object.defineProperty(Element.prototype,'myStyle',{ | |
| get: function() { | |
| var style = this.style; | |
| if(!style.parentElement) style.parentElement = this; | |
| return style; | |
| } | |
| }); | |
| // | |
| // load all stylesheets at the time the script is loaded | |
| // then do it again when all stylesheets are downloaded | |
| // and again if some style tag is added to the DOM | |
| // | |
| if(!("no_auto_stylesheet_detection" in window)) { | |
| cssCascade.loadAllStyleSheets(); | |
| document.addEventListener("DOMContentLoaded", function() { | |
| cssCascade.loadAllStyleSheets(); | |
| querySelectorLive( | |
| cssCascade.selectorForStylesheets, | |
| { | |
| onadded: function(e) { | |
| // TODO: respect DOM order? | |
| cssCascade.loadStyleSheetTag(e); | |
| cssCascade.dispatchEvent('stylesheetadded'); | |
| } | |
| } | |
| ) | |
| }) | |
| } | |
| return cssCascade; | |
| })(window, document); | |
| require.define('src/core/css-cascade.js'); | |
| //////////////////////////////////////// | |
| module.exports = (function(window, document) { "use strict"; | |
| var cssSyntax = require('src/core/css-syntax.js'); | |
| var cssCascade = require('src/core/css-cascade.js'); | |
| var cssBreak = { | |
| // | |
| // returns true if an element is replaced | |
| // (can't be broken because considered as an image in css layout) | |
| // | |
| isReplacedElement: function isReplacedElement(element) { | |
| if(!(element instanceof Element)) return false; | |
| var replacedElementTags = /^(SVG|MATH|IMG|VIDEO|PICTURE|OBJECT|EMBED|IFRAME|TEXTAREA|BUTTON|INPUT)$/; // TODO: more | |
| return replacedElementTags.test(element.tagName); | |
| }, | |
| // | |
| // returns true if an element has a scrollbar or act on overflowing content | |
| // | |
| isScrollable: function isScrollable(element, elementOverflow) { | |
| if(!(element instanceof Element)) return false; | |
| if(typeof(elementOverflow)=="undefined") elementOverflow = getComputedStyle(element).overflow; | |
| return ( | |
| elementOverflow !== "visible" | |
| && elementOverflow !== "hidden" | |
| ); | |
| }, | |
| // | |
| // returns true if the element is part of an inline flow | |
| // TextNodes definitely qualify, but also inline-block elements | |
| // | |
| isSingleLineOfTextComponent: function(element, elementStyle, elementDisplay, elementPosition, isReplaced) { | |
| if(!(element instanceof Element)) return true; | |
| if(typeof(elementStyle)=="undefined") elementStyle = getComputedStyle(element); | |
| if(typeof(elementDisplay)=="undefined") elementDisplay = elementStyle.display; | |
| if(typeof(elementPosition)=="undefined") elementPosition = elementStyle.position; | |
| if(typeof(isReplaced)=="undefined") isReplaced = this.isReplacedElement(element); | |
| return ( | |
| elementDisplay === "inline-block" | |
| || elementDisplay === "inline-table" | |
| || elementDisplay === "inline-flex" | |
| || elementDisplay === "inline-grid" | |
| // TODO: more | |
| ) && ( | |
| elementPosition === "static" | |
| || elementPosition === "relative" | |
| ); | |
| }, | |
| // | |
| // returns true if the element is part of an inline flow | |
| // TextNodes definitely qualify, but also inline-block elements | |
| // | |
| hasAnyInlineFlow: function(element) { | |
| function countAsInline(element) { | |
| if(!(element instanceof Element)) return !(/^\s*$/.test(element.nodeValue)); | |
| return !cssBreak.isOutOfFlowElement(element) && cssBreak.isSingleLineOfTextComponent(element); | |
| } | |
| // try to find any inline element | |
| var current = element.firstChild; | |
| while(current) { | |
| if(countAsInline(current)) return true; | |
| current = current.nextSibling; | |
| } | |
| // no inline element | |
| return false; | |
| }, | |
| // | |
| // returns true if the element breaks the inline flow | |
| // (the case of block elements, mostly) | |
| // | |
| isLineBreakingElement: function(element, elementStyle, elementDisplay, elementPosition) { | |
| if(!(element instanceof Element)) return false; | |
| if(typeof(elementStyle)=="undefined") elementStyle = getComputedStyle(element); | |
| if(typeof(elementDisplay)=="undefined") elementDisplay = elementStyle.display; | |
| if(typeof(elementPosition)=="undefined") elementPosition = elementStyle.position; | |
| return ( | |
| ( | |
| // in-flow bock elements | |
| (elementDisplay === "block") | |
| && !this.isOutOfFlowElement(element, elementStyle, elementDisplay, elementPosition) | |
| ) || ( | |
| // displayed <br> elements | |
| element.tagName==="BR" && elementDisplay!=="none" | |
| ) | |
| ); | |
| }, | |
| // | |
| // returns true if the element breaks the inline flow before him | |
| // (the case of block elements, mostly) | |
| // | |
| isLinePreBreakingElement: function(element, elementStyle, elementDisplay, elementPosition) { | |
| if(!(element instanceof Element)) return false; | |
| var breakBefore = cssCascade.getSpecifiedStyle(element,'break-before').toCSSString(); | |
| return ( | |
| (breakBefore=="region"||breakBefore=="all") | |
| || cssBreak.isLineBreakingElement(element, elementStyle, elementDisplay, elementPosition) | |
| ); | |
| }, | |
| // | |
| // returns true if the element breaks the inline flow after him | |
| // (the case of block elements, mostly) | |
| // | |
| isLinePostBreakingElement: function(element, elementStyle, elementDisplay, elementPosition) { | |
| if(!(element instanceof Element)) return false; | |
| var breakAfter = cssCascade.getSpecifiedStyle(element,'break-after').toCSSString(); | |
| return ( | |
| (breakAfter=="region"||breakAfter=="all") | |
| || cssBreak.isLineBreakingElement(element, elementStyle, elementDisplay, elementPosition) | |
| ); | |
| }, | |
| // | |
| // returns true if the element is outside any block/inline flow | |
| // (this the case of absolutely positioned elements, and floats) | |
| // | |
| isOutOfFlowElement: function(element, elementStyle, elementDisplay, elementPosition, elementFloat) { | |
| if(!(element instanceof Element)) return false; | |
| if(typeof(elementStyle)=="undefined") elementStyle = getComputedStyle(element); | |
| if(typeof(elementDisplay)=="undefined") elementDisplay = elementStyle.display; | |
| if(typeof(elementPosition)=="undefined") elementPosition = elementStyle.position; | |
| if(typeof(elementFloat)=="undefined") elementFloat = elementStyle.float || elementStyle.styleFloat || elementStyle.cssFloat; | |
| return ( | |
| // positioned elements are out of the flow | |
| (elementPosition==="absolute"||elementPosition==="fixed") | |
| // floated elements as well | |
| || (elementFloat!=="none") | |
| // not sure but let's say hidden elements as well | |
| || (elementDisplay==="none") | |
| ); | |
| }, | |
| // | |
| // returns true if two sibling elements are in the same text line | |
| // (this function is not perfect, work with it with care) | |
| // | |
| areInSameSingleLine: function areInSameSingleLine(element1, element2) { | |
| // | |
| // look for obvious reasons why it wouldn't be the case | |
| // | |
| // if the element are not direct sibling, we must use their inner siblings as well | |
| if(element1.nextSibling != element2) { | |
| if(element2.nextSibling != element1) throw "I gave up!"; | |
| var t = element1; element1=element2; element2=t; | |
| } | |
| // a block element is never on the same line as another element | |
| if(this.isLinePostBreakingElement(element1)) return false; | |
| if(this.isLinePreBreakingElement(element2)) return false; | |
| // if the previous element is out of flow, we may consider it as being part of the current line | |
| if(this.isOutOfFlowElement(element1)) return true; | |
| // if the current object is not a single line component, return false | |
| if(!this.isSingleLineOfTextComponent(element1)) return false; | |
| // | |
| // compute the in-flow bounding rect of the two elements | |
| // | |
| var element1box = Node.getBoundingClientRect(element1); | |
| var element2box = Node.getBoundingClientRect(element2); | |
| function shift(box,shiftX,shiftY) { | |
| return { | |
| top: box.top+shiftY, | |
| bottom: box.bottom+shiftY, | |
| left: box.left+shiftX, | |
| right: box.right+shiftX | |
| } | |
| } | |
| // we only need to shift elements | |
| if(element1 instanceof Element) { | |
| var element1Style = getComputedStyle(element1); | |
| element1box = shift(element1box, parseFloat(element1Style.marginLeft), parseFloat(element1Style.marginTop)) | |
| if(element1Style.position=="relative") { | |
| element1box = shift(element1box, parseFloat(element1Style.left), parseFloat(element1Style.top)) | |
| } | |
| } | |
| // we only need to shift elements | |
| if(element2 instanceof Element) { | |
| var element2Style = getComputedStyle(element2); | |
| element2box = shift(element2box, parseFloat(element2Style.marginLeft), parseFloat(element2Style.marginTop)) | |
| if(element2Style.position=="relative") { | |
| element2box = shift(element2box, parseFloat(element2Style.left), parseFloat(element2Style.top)) | |
| } | |
| } | |
| // order the nodes so that they are in left-to-right order | |
| // (this means invert their order in the case of right-to-left flow) | |
| var firstElement = getComputedStyle(element1.parentNode).direction=="rtl" ? element2box : element1box; | |
| var secondElement = getComputedStyle(element1.parentNode).direction=="rtl" ? element1box : element2box; | |
| // return true if both elements are have non-overlapping | |
| // margin- and position-corrected in-flow bounding rect | |
| // and if their relative position is the one of the current | |
| // flow (either rtl or ltr) | |
| return firstElement.right <= secondElement.left; | |
| // TODO: what about left-to-right + right-aligned text? | |
| // I should probably takes care of vertical position in this case to solve ambiguities | |
| }, | |
| // | |
| // returns true if the element has "overflow: hidden" set on it, and actually overflows | |
| // | |
| isHiddenOverflowing: function isHiddenOverflowing(element, elementOverflow) { | |
| if(!(element instanceof Element)) return false; | |
| if(typeof(elementOverflow)=="undefined") elementOverflow = getComputedStyle(element).display; | |
| return ( | |
| elementOverflow == "hidden" | |
| && element.offsetHeight != element.scrollHeight // trust me that works | |
| ); | |
| }, | |
| // | |
| // returns true if the element has a border-radius that impacts his layout | |
| // | |
| hasBigRadius: function(element, elementStyle) { | |
| if(!(element instanceof Element)) return false; | |
| if(typeof(elementStyle)=="undefined") elementStyle = getComputedStyle(element); | |
| // if the browser supports radiuses {f### prefixes} | |
| if("borderTopLeftRadius" in elementStyle) { | |
| var tlRadius = parseFloat(elementStyle.borderTopLeftRadius); | |
| var trRadius = parseFloat(elementStyle.borderTopRightRadius); | |
| var blRadius = parseFloat(elementStyle.borderBottomLeftRadius); | |
| var brRadius = parseFloat(elementStyle.borderBottomRightRadius); | |
| // tiny radiuses (<15px) are tolerated anyway | |
| if(tlRadius < 15 && trRadius < 15 && blRadius < 15 && brRadius < 15) { | |
| return false; | |
| } | |
| var tWidth = parseFloat(elementStyle.borderTopWidth); | |
| var bWidth = parseFloat(elementStyle.borderBottomWidth); | |
| var lWidth = parseFloat(elementStyle.borderLeftWidth); | |
| var rWidth = parseFloat(elementStyle.borderRightWidth); | |
| // make sure the radius itself is contained into the border | |
| if(tlRadius > tWidth) return true; | |
| if(tlRadius > lWidth) return true; | |
| if(trRadius > tWidth) return true; | |
| if(trRadius > rWidth) return true; | |
| if(blRadius > bWidth) return true; | |
| if(blRadius > lWidth) return true; | |
| if(brRadius > bWidth) return true; | |
| if(brRadius > rWidth) return true; | |
| } | |
| // all conditions were met | |
| return false; | |
| }, | |
| // | |
| // return trus if the break-inside property is 'avoid' or 'avoid-region' | |
| // | |
| isBreakInsideAvoid: function isBreakInsideAvoid(element, elementStyle) { | |
| var breakInside = cssCascade.getSpecifiedStyle(element, 'break-inside', undefined, true).toCSSString().trim().toLowerCase(); | |
| return (breakInside == "avoid" || breakInside == "avoid-region"); | |
| }, | |
| // | |
| // returns true if the element is unbreakable according to the spec | |
| // (and some of the expected limitations of HTML/CSS) | |
| // | |
| isMonolithic: function isMonolithic(element) { | |
| if(!(element instanceof Element)) return false; | |
| var elementStyle = getComputedStyle(element); | |
| var elementOverflow = elementStyle.overflow; | |
| var elementDisplay = elementStyle.display; | |
| // Some content is not fragmentable, for example: | |
| // - many types of replaced elements (such as images or video) | |
| var isReplaced = this.isReplacedElement(element); | |
| // - scrollable elements | |
| var isScrollable = this.isScrollable(element, elementOverflow); | |
| // - a single line of text content. | |
| var isSingleLineOfText = this.isSingleLineOfTextComponent(element, elementStyle, elementDisplay, undefined, isReplaced); | |
| // Such content is considered monolithic: it contains no | |
| // possible break points. | |
| // In addition to any content which is not fragmentable, | |
| // UAs may consider as monolithic: | |
| // - any elements with ‘overflow’ set to ‘auto’ or ‘scroll’ | |
| // - any elements with ‘overflow: hidden’ and a non-‘auto’ logical height (and no specified maximum logical height). | |
| var isHiddenOverflowing = this.isHiddenOverflowing(element, elementOverflow); | |
| // ADDITION TO THE SPEC: | |
| // I don't want to handle the case where | |
| // an element has a border-radius that is bigger | |
| // than the border-width to which it belongs | |
| var hasBigRadius = this.hasBigRadius(element, elementStyle); | |
| // ADDITION TO THE SPEC: | |
| // Someone proposed to support "break-inside: avoid" here | |
| var isBreakInsideAvoid = this.isBreakInsideAvoid(element, elementStyle); | |
| // all of them are monolithic | |
| return isReplaced || isScrollable || isSingleLineOfText || isHiddenOverflowing || hasBigRadius || isBreakInsideAvoid; | |
| }, | |
| // | |
| // returns true if "r" is a collapsed range located at a possible break point for "region" | |
| // (this function does all the magic for you, but you may want to avoid using it too much) | |
| // | |
| isPossibleBreakPoint: function isPossibleBreakPoint(r, region) { | |
| // r has to be a range, and be collapsed | |
| if(!(r instanceof Range)) return false; | |
| if(!(r.collapsed)) return false; | |
| // no ancestor up to the region has to be monolithic | |
| var ancestor = r.startContainer; | |
| while(ancestor && ancestor !== region) { | |
| if(cssBreak.isMonolithic(ancestor)) { | |
| return false; | |
| } | |
| ancestor = ancestor.parentNode; | |
| } | |
| // we also have to check that we're not between two single-line-of-text elements | |
| // that are actually on the same line (in which case you can't break) | |
| var ancestor = r.startContainer; | |
| var lastAncestor = r.startContainer.childNodes[r.startOffset]; | |
| while(ancestor && lastAncestor !== region) { | |
| if(lastAncestor && lastAncestor.previousSibling) { | |
| if(this.areInSameSingleLine(lastAncestor, lastAncestor.previousSibling)) { | |
| return false; | |
| } | |
| } | |
| lastAncestor = ancestor; | |
| ancestor = ancestor.parentNode; | |
| } | |
| // there are some very specific conditions for breaking | |
| // at the edge of an element: | |
| if(r.startOffset==0) { | |
| // Class 3 breaking point: | |
| // ======================== | |
| // Between the content edge of a block container box | |
| // and the outer edges of its child content (margin | |
| // edges of block-level children or line box edges | |
| // for inline-level children) if there is a (non-zero) | |
| // gap between them. | |
| var firstChild = r.startContainer.childNodes[0]; | |
| if(firstChild) { | |
| var firstChildBox = ( | |
| Node.getBoundingClientRect(firstChild) | |
| ); | |
| var parentBox = ( | |
| r.startContainer.getBoundingClientRect() | |
| ); | |
| if(firstChildBox.top == parentBox.top) { | |
| return false; | |
| } | |
| } else { | |
| return false; | |
| } | |
| } | |
| // all conditions are met! | |
| return true; | |
| } | |
| }; | |
| return cssBreak; | |
| })(window, document); | |
| require.define('src/core/css-break.js'); | |
| //////////////////////////////////////// | |
| "use strict"; | |
| // | |
| // start by polyfilling caretRangeFromPoint | |
| // | |
| if(!document.caretRangeFromPoint) { | |
| if (document.caretPositionFromPoint) { | |
| document.caretRangeFromPoint = function caretRangeFromPoint(x,y) { | |
| var r = document.createRange(); | |
| var p = document.caretPositionFromPoint(x,y); | |
| if(p.offsetNode) { | |
| r.setStart(p.offsetNode, p.offset); | |
| r.setEnd(p.offsetNode, p.offset); | |
| } | |
| return r; | |
| } | |
| } else if((document.body||document.createElement('body')).createTextRange) { | |
| // | |
| // we may want to convert TextRange to Range | |
| // | |
| var TextRangeUtils = { | |
| convertToDOMRange: function (textRange, document) { | |
| var adoptBoundary = function(domRange, textRangeInner, bStart) { | |
| // iterate backwards through parent element to find anchor location | |
| var cursorNode = document.createElement('a'), cursor = textRangeInner.duplicate(); | |
| cursor.collapse(bStart); | |
| var parent = cursor.parentElement(); | |
| do { | |
| parent.insertBefore(cursorNode, cursorNode.previousSibling); | |
| cursor.moveToElementText(cursorNode); | |
| } while (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRangeInner) > 0 && cursorNode.previousSibling); | |
| // when we exceed or meet the cursor, we've found the node | |
| if (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRangeInner) == -1 && cursorNode.nextSibling) { | |
| // data node | |
| cursor.setEndPoint(bStart ? 'EndToStart' : 'EndToEnd', textRangeInner); | |
| domRange[bStart ? 'setStart' : 'setEnd'](cursorNode.nextSibling, cursor.text.length); | |
| } else { | |
| // element | |
| domRange[bStart ? 'setStartBefore' : 'setEndBefore'](cursorNode); | |
| } | |
| cursorNode.parentNode.removeChild(cursorNode); | |
| } | |
| // validate arguments | |
| if(!document) { document=window.document; } | |
| // return a DOM range | |
| var domRange = document.createRange(); | |
| adoptBoundary(domRange, textRange, true); | |
| adoptBoundary(domRange, textRange, false); | |
| return domRange; | |
| }, | |
| convertFromDOMRange: function (domRange) { | |
| var adoptEndPoint = function(textRange, domRangeInner, bStart) { | |
| // find anchor node and offset | |
| var container = domRangeInner[bStart ? 'startContainer' : 'endContainer']; | |
| var offset = domRangeInner[bStart ? 'startOffset' : 'endOffset'], textOffset = 0; | |
| var anchorNode = DOMUtils.isDataNode(container) ? container : container.childNodes[offset]; | |
| var anchorParent = DOMUtils.isDataNode(container) ? container.parentNode : container; | |
| // visible data nodes need a text offset | |
| if (container.nodeType == 3 || container.nodeType == 4) | |
| textOffset = offset; | |
| // create a cursor element node to position range (since we can't select text nodes) | |
| var cursorNode = domRangeInner._document.createElement('a'); | |
| anchorParent.insertBefore(cursorNode, anchorNode); | |
| var cursor = domRangeInner._document.body.createTextRange(); | |
| cursor.moveToElementText(cursorNode); | |
| cursorNode.parentNode.removeChild(cursorNode); | |
| // move range | |
| textRange.setEndPoint(bStart ? 'StartToStart' : 'EndToStart', cursor); | |
| textRange[bStart ? 'moveStart' : 'moveEnd']('character', textOffset); | |
| } | |
| // return an IE text range | |
| var textRange = domRange._document.body.createTextRange(); | |
| adoptEndPoint(textRange, domRange, true); | |
| adoptEndPoint(textRange, domRange, false); | |
| return textRange; | |
| } | |
| }; | |
| document.caretRangeFromPoint = function caretRangeFromPoint(x,y) { | |
| // the accepted number of vertical backtracking, in CSS pixels | |
| var IYDepth = 40; | |
| // try to create a text range at the specified location | |
| var r = document.body.createTextRange(); | |
| for(var iy=IYDepth; iy; iy=iy-4) { | |
| var ix = x; if(true) { | |
| try { | |
| r.moveToPoint(ix,iy+y-IYDepth); | |
| return TextRangeUtils.convertToDOMRange(r); | |
| } catch(ex) {} | |
| } | |
| } | |
| // if that fails, return the location just after the element located there | |
| try { | |
| var elem = document.elementFromPoint(x-1,y-1); | |
| var r = document.createRange(); | |
| r.setStartAfter(elem); | |
| return r; | |
| } catch(ex) { | |
| return null; | |
| } | |
| } | |
| } | |
| } | |
| /// | |
| /// helper function for moving ranges char by char | |
| /// | |
| Range.prototype.myMoveOneCharLeft = function() { | |
| var r = this; | |
| // move to the previous cursor location | |
| if(r.endOffset > 0) { | |
| // if we can enter into the previous sibling | |
| var previousSibling = r.endContainer.childNodes[r.endOffset-1]; | |
| if(previousSibling && previousSibling.lastChild) { | |
| // enter the previous sibling from its end | |
| r.setEndAfter(previousSibling.lastChild); | |
| } else if(previousSibling && previousSibling.nodeType==previousSibling.TEXT_NODE) { // todo: lookup value | |
| // enter the previous text node from its end | |
| r.setEnd(previousSibling, previousSibling.nodeValue.length); | |
| } else { | |
| // else move before that element | |
| r.setEnd(r.endContainer, r.endOffset-1); | |
| } | |
| } else { | |
| r.setEndBefore(r.endContainer); | |
| } | |
| } | |
| Range.prototype.myMoveOneCharRight = function() { | |
| var r = this; | |
| // move to the previous cursor location | |
| var max = (r.startContainer.nodeType==r.startContainer.TEXT_NODE ? r.startContainer.nodeValue.length : r.startContainer.childNodes.length) | |
| if(r.startOffset < max) { | |
| // if we can enter into the next sibling | |
| var nextSibling = r.endContainer.childNodes[r.endOffset]; | |
| if(nextSibling && nextSibling.firstChild) { | |
| // enter the next sibling from its start | |
| r.setStartBefore(nextSibling.firstChild); | |
| } else if(nextSibling && nextSibling.nodeType==nextSibling.TEXT_NODE && nextSibling.nodeValue!='') { // todo: lookup value | |
| // enter the next text node from its start | |
| r.setStart(nextSibling, 0); | |
| } else { | |
| // else move before that element | |
| r.setStart(r.startContainer, r.startOffset+1); | |
| } | |
| } else { | |
| r.setStartAfter(r.endContainer); | |
| } | |
| // shouldn't be needed but who knows... | |
| r.setEnd(r.startContainer, r.startOffset); | |
| } | |
| /// | |
| /// This functions is optimized to not yield inside a word in a text node | |
| /// | |
| Range.prototype.myMoveTowardRight = function() { | |
| var r = this; | |
| // move to the previous cursor location | |
| var isTextNode = r.startContainer.nodeType==r.startContainer.TEXT_NODE; | |
| var max = (isTextNode ? r.startContainer.nodeValue.length : r.startContainer.childNodes.length) | |
| if(r.startOffset < max) { | |
| // if we can enter into the next sibling | |
| var nextSibling = r.endContainer.childNodes[r.endOffset]; | |
| if(nextSibling && nextSibling.firstChild) { | |
| // enter the next sibling from its start | |
| r.setStartBefore(nextSibling.firstChild); | |
| } else if(nextSibling && nextSibling.nodeType==nextSibling.TEXT_NODE && nextSibling.nodeValue!='') { // todo: lookup value | |
| // enter the next text node from its start | |
| r.setStart(nextSibling, 0); | |
| } else if(isTextNode) { | |
| // move to the next non a-zA-Z symbol | |
| var currentText = r.startContainer.nodeValue; | |
| var currentOffset = r.startOffset; | |
| var currentLetter = currentText[currentOffset++]; | |
| while(currentOffset < max && /^\w$/.test(currentLetter)) { | |
| currentLetter = currentText[currentOffset++]; | |
| } | |
| r.setStart(r.startContainer, currentOffset); | |
| } else { | |
| // else move after that element | |
| r.setStart(r.startContainer, r.startOffset+1); | |
| } | |
| } else { | |
| r.setStartAfter(r.endContainer); | |
| } | |
| // shouldn't be needed but who knows... | |
| r.setEnd(r.startContainer, r.startOffset); | |
| } | |
| Range.prototype.myMoveEndOneCharLeft = function() { | |
| var r = this; | |
| // move to the previous cursor location | |
| if(r.endOffset > 0) { | |
| // if we can enter into the previous sibling | |
| var previousSibling = r.endContainer.childNodes[r.endOffset-1]; | |
| if(previousSibling && previousSibling.lastChild) { | |
| // enter the previous sibling from its end | |
| r.setEndAfter(previousSibling.lastChild); | |
| } else if(previousSibling && previousSibling.nodeType==previousSibling.TEXT_NODE) { // todo: lookup value | |
| // enter the previous text node from its end | |
| r.setEnd(previousSibling, previousSibling.nodeValue.length); | |
| } else { | |
| // else move before that element | |
| r.setEnd(r.endContainer, r.endOffset-1); | |
| } | |
| } else { | |
| r.setEndBefore(r.endContainer); | |
| } | |
| } | |
| Range.prototype.myMoveEndOneCharRight = function() { | |
| var r = this; | |
| // move to the previous cursor location | |
| var max = (r.endContainer.nodeType==r.endContainer.TEXT_NODE ? r.endContainer.nodeValue.length : r.endContainer.childNodes.length) | |
| if(r.endOffset < max) { | |
| // if we can enter into the next sibling | |
| var nextSibling = r.endContainer.childNodes[r.endOffset]; | |
| if(nextSibling && nextSibling.firstChild) { | |
| // enter the next sibling from its start | |
| r.setEndBefore(nextSibling.firstChild); | |
| } else if(nextSibling && nextSibling.nodeType==nextSibling.TEXT_NODE) { // todo: lookup value | |
| // enter the next text node from its start | |
| r.setEnd(nextSibling, 0); | |
| } else { | |
| // else move before that element | |
| r.setEnd(r.endContainer, r.endOffset+1); | |
| } | |
| } else { | |
| r.setEndAfter(r.endContainer); | |
| } | |
| } | |
| // | |
| // Get the *real* bounding client rect of the range | |
| // { therefore we need to fix some browser bugs... } | |
| // | |
| Range.prototype.myGetSelectionRect = function() { | |
| // get the browser's claimed rect | |
| var rect = this.getBoundingClientRect(); | |
| // HACK FOR ANDROID BROWSER AND OLD WEBKIT | |
| if(!rect) { | |
| rect={top:0,right:0,bottom:0,left:0,width:0,height:0}; | |
| } | |
| // if the value seems wrong... (some browsers don't like collapsed selections) | |
| if(this.collapsed && rect.top===0 && rect.bottom===0) { | |
| // select one char and infer location | |
| var clone = this.cloneRange(); var collapseToLeft=false; clone.collapse(false); | |
| // the case where no char before is tricky... | |
| if(clone.startOffset==0) { | |
| // let's move on char to the right | |
| clone.myMoveTowardRight(); | |
| collapseToLeft=true; | |
| // note: some browsers don't like selections | |
| // that spans multiple containers, so we will | |
| // iterate this process until we have one true | |
| // char selected | |
| clone.setStart(clone.endContainer, 0); | |
| } else { | |
| // else, just select the char before | |
| clone.setStart(this.startContainer, this.startOffset-1); | |
| collapseToLeft=false; | |
| } | |
| // get some real rect | |
| var rect = clone.myGetSelectionRect(); | |
| // compute final value | |
| if(collapseToLeft) { | |
| return { | |
| left: rect.left, | |
| right: rect.left, | |
| width: 0, | |
| top: rect.top, | |
| bottom: rect.bottom, | |
| height: rect.height | |
| } | |
| } else { | |
| return { | |
| left: rect.right, | |
| right: rect.right, | |
| width: 0, | |
| top: rect.top, | |
| bottom: rect.bottom, | |
| height: rect.height | |
| } | |
| } | |
| } else { | |
| return rect; | |
| } | |
| } | |
| // not sure it's needed but still | |
| if(!window.Element) window.Element=window.HTMLElement; | |
| if(!window.Node) window.Node = {}; | |
| // make getBCR working on text nodes & stuff | |
| Node.getBoundingClientRect = function getBoundingClientRect(element) { | |
| if (element.getBoundingClientRect) { | |
| var rect = element.getBoundingClientRect(); | |
| } else { | |
| var range = document.createRange(); | |
| range.selectNode(element); | |
| var rect = range.getBoundingClientRect(); | |
| } | |
| // HACK FOR ANDROID BROWSER AND OLD WEBKIT | |
| if(!rect) { | |
| rect={top:0,right:0,bottom:0,left:0,width:0,height:0}; | |
| } | |
| return rect; | |
| }; | |
| // make getCR working on text nodes & stuff | |
| Node.getClientRects = function getClientRects(firstChild) { | |
| if (firstChild.getBoundingClientRect) { | |
| return firstChild.getClientRects(); | |
| } else { | |
| var range = document.createRange(); | |
| range.selectNode(firstChild); | |
| return range.getClientRects(); | |
| } | |
| }; | |
| // fix for IE (contains fails for text nodes...) | |
| Node.contains = function contains(parentNode,node) { | |
| if(node.nodeType != 1) { | |
| if(!node.parentNode) return false; | |
| return node.parentNode==parentNode || parentNode.contains(node.parentNode); | |
| } else { | |
| return parentNode.contains(node); | |
| } | |
| } | |
| // | |
| // get the bounding rect of the selection, including the bottom padding/marging of the previous element if required | |
| // { this is a special version for breaking algorithms that do not want to miss the previous element real size } | |
| // | |
| Range.prototype.myGetExtensionRect = function() { | |
| // this function returns the selection rect | |
| // but does take care of taking in account | |
| // the bottom-{padding/border} of the previous | |
| // sibling element, to detect overflow points | |
| // more accurately | |
| var rect = this.myGetSelectionRect(); | |
| var previousSibling = this.endContainer.childNodes[this.endOffset-1]; | |
| if(previousSibling) { | |
| // correct with the new take | |
| var prevSibRect = Node.getBoundingClientRect(previousSibling); | |
| var adjustedBottom = Math.max(rect.bottom,prevSibRect.bottom); | |
| if(adjustedBottom == rect.bottom) return rect; | |
| return { | |
| left: rect.left, | |
| right: rect.right, | |
| width: rect.width, | |
| top: rect.top, | |
| bottom: adjustedBottom, | |
| height: adjustedBottom - rect.top | |
| }; | |
| } else if(this.bottom==0 && this.endContainer.nodeType === 3) { | |
| // note that if we are in a text node, | |
| // we may want to cover all the previous | |
| // text in the node to avoid whitespace | |
| // related bugs | |
| var onlyWhiteSpaceBefore = /^(\s|\n)*$/.test(this.endContainer.nodeValue.substr(0,this.endOffset)); | |
| if(onlyWhiteSpaceBefore) { | |
| // if we are in the fucking whitespace land, return first line | |
| var prevSibRect = Node.getClientRects(this.endContainer)[0]; | |
| return prevSibRect; | |
| } else { | |
| // otherwhise, let's rely on previous chars | |
| var auxiliaryRange = this.cloneRange(); | |
| auxiliaryRange.setStart(this.endContainer,0); | |
| // correct with the new take | |
| var prevSibRect = auxiliaryRange.getBoundingClientRect(); | |
| var adjustedBottom = Math.max(rect.bottom,prevSibRect.bottom); | |
| return { | |
| left: rect.left, | |
| right: rect.right, | |
| width: rect.width, | |
| top: rect.top, | |
| bottom: adjustedBottom, | |
| height: adjustedBottom - rect.top | |
| }; | |
| } | |
| } else { | |
| return rect; | |
| } | |
| } | |
| require.define('src/css-regions/lib/range-extensions.js'); | |
| //////////////////////////////////////// | |
| // | |
| // this module holds the big-picture actions of the polyfill | |
| // | |
| module.exports = (function(window, document) { "use strict"; | |
| var domEvents = require('src/core/dom-events.js'); | |
| var cssSyntax = require('src/core/css-syntax.js'); | |
| var cssCascade = require('src/core/css-cascade.js'); | |
| var cssBreak = require('src/core/css-break.js'); | |
| var cssRegionsHelpers = window.cssRegionsHelpers = { | |
| // | |
| // returns the previous sibling of the element | |
| // or the previous sibling of its nearest ancestor that has one | |
| // | |
| getAllLevelPreviousSibling: function(e, region) { | |
| if(!e || e==region) return null; | |
| // find the nearest ancestor that has a previous sibling | |
| while(!e.previousSibling) { | |
| // but bubble to the next avail ancestor | |
| e = e.parentNode; | |
| // dont get over the bar | |
| if(!e || e==region) return null; | |
| } | |
| // return that sibling | |
| return e.previousSibling; | |
| }, | |
| // | |
| // prepares the element to become a css region | |
| // | |
| markNodesAsRegion: function(nodes,fast) { | |
| nodes.forEach(function(node) { | |
| node.regionOverset = 'empty'; | |
| node.setAttribute('data-css-region',node.cssRegionsLastFlowFromName); | |
| cssRegionsHelpers.hideTextNodesFromFragmentSource([node]); | |
| node.cssRegionsWrapper = node.cssRegionsWrapper || node.appendChild(document.createElement("cssregion")); | |
| }); | |
| }, | |
| // | |
| // prepares the element to return to its normal css life | |
| // | |
| unmarkNodesAsRegion: function(nodes,fast) { | |
| nodes.forEach(function(node) { | |
| // restore regionOverset to its natural value | |
| node.regionOverset = 'fit'; | |
| // remove the current <cssregion> tag | |
| try { node.cssRegionsWrapper && node.removeChild(node.cssRegionsWrapper); } | |
| catch(ex) { setImmediate(function() { throw ex })}; | |
| node.cssRegionsWrapper = undefined; | |
| delete node.cssRegionsWrapper; | |
| // restore top-level texts that may have been hidden | |
| cssRegionsHelpers.unhideTextNodesFromFragmentSource([node]); | |
| // unmark as a region | |
| node.removeAttribute('data-css-region'); | |
| }); | |
| }, | |
| // | |
| // prepares the element for cloning (mainly give them an ID) | |
| // | |
| fragmentSourceIndex: 0, | |
| markNodesAsFragmentSource: function(nodes,ignoreRoot) { | |
| function visit(node,k) { | |
| var child, next; | |
| switch (node.nodeType) { | |
| case 1: // Element node | |
| if(typeof(k)=="undefined" || !ignoreRoot) { | |
| // mark as fragment source | |
| var id = node.getAttributeNode('data-css-regions-fragment-source'); | |
| if(!id) { node.setAttribute('data-css-regions-fragment-source', cssRegionsHelpers.fragmentSourceIndex++); } | |
| } | |
| node.setAttribute('data-css-regions-cloning', true); | |
| // expand list values | |
| if(node.tagName=='OL') cssRegionsHelpers.expandListValues(node); | |
| if(typeof(k)!="undefined" && node.tagName=="LI") cssRegionsHelpers.expandListValues(node.parentNode); | |
| case 9: // Document node | |
| case 11: // Document fragment node | |
| child = node.firstChild; | |
| while (child) { | |
| next = child.nextSibling; | |
| visit(child); | |
| child = next; | |
| } | |
| break; | |
| } | |
| } | |
| nodes.forEach(visit); | |
| }, | |
| // | |
| // computes the "value" attribute of every LI element out there | |
| // | |
| expandListValues: function(OL) { | |
| if(OL.getAttribute("data-css-li-value-expanded")) return; | |
| OL.setAttribute('data-css-li-value-expanded', true); | |
| if(OL.hasAttribute("reversed")) { | |
| var currentValue = OL.getAttribute("start") ? parseInt(OL.getAttribute("start")) : OL.childElementCount; | |
| var increment = -1; | |
| } else { | |
| var currentValue = OL.getAttribute("start") ? parseInt(OL.getAttribute("start")) : 1; | |
| var increment = +1; | |
| } | |
| var LI = OL.firstElementChild; var LIV = null; | |
| while(LI) { | |
| if(LI.tagName==="LI") { | |
| if(LIV=LI.getAttributeNode("value")) { | |
| currentValue = parseInt(LIV.nodeValue); | |
| LI.setAttribute('data-css-old-value', currentValue) | |
| } else { | |
| LI.setAttribute("value", currentValue); | |
| } | |
| currentValue = currentValue + increment; | |
| } | |
| LI = LI.nextElementSibling; | |
| } | |
| }, | |
| // | |
| // reverts to automatic computation of the value of LI elements | |
| // | |
| unexpandListValues: function(OL) { | |
| if(!OL.hasAttribute('data-css-li-value-expanded')) return; | |
| OL.removeAttribute('data-css-li-value-expanded') | |
| var LI = OL.firstElementChild; var LIV = null; | |
| while(LI) { | |
| if(LI.tagName==="LI") { | |
| if(LIV=LI.getAttributeNode("data-css-old-value")) { | |
| LI.removeAttributeNode(LIV); | |
| } else { | |
| LI.removeAttribute('value'); | |
| } | |
| } | |
| LI = LI.nextElementSibling; | |
| } | |
| }, | |
| // | |
| // makes empty text nodes which cannot get "display: none" applied to them | |
| // | |
| listOfTextNodesForIE: [], | |
| hideTextNodesFromFragmentSource: function(nodes) { | |
| function visit(node,k) { | |
| var child, next; | |
| switch (node.nodeType) { | |
| case 3: // Text node | |
| if(!node.parentNode.getAttribute('data-css-regions-fragment-source')) { | |
| // we have to remove their content the hard way... | |
| node.cssRegionsSavedNodeValue = node.nodeValue; | |
| node.nodeValue = ""; | |
| // HACK: OTHERWISE IE WILL GC THE TEXTNODE AND RETURNS YOU | |
| // A FRESH TEXTNODE THE NEXT TIME WHERE YOUR EXPANDO | |
| // IS NOWHERE TO BE SEEN! | |
| if(navigator.userAgent.indexOf('MSIE')>0 || navigator.userAgent.indexOf("Trident")>0) { | |
| if(cssRegionsHelpers.listOfTextNodesForIE.indexOf(node)==-1) { | |
| cssRegionsHelpers.listOfTextNodesForIE.push(node); | |
| } | |
| } | |
| } | |
| break; | |
| case 1: // Element node | |
| if(node.hasAttribute('data-css-regions-cloning')) { | |
| node.removeAttribute('data-css-regions-cloning'); | |
| node.setAttribute('data-css-regions-cloned', true); | |
| if(node.currentStyle) node.currentStyle.display.toString(); // IEFIX FOR BAD STYLE RECALC | |
| } | |
| if(typeof(k)=="undefined") return; | |
| case 9: // Document node | |
| case 11: // Document fragment node | |
| child = node.firstChild; | |
| while (child) { | |
| next = child.nextSibling; | |
| visit(child); | |
| child = next; | |
| } | |
| break; | |
| } | |
| } | |
| nodes.forEach(visit); | |
| }, | |
| // | |
| // makes emptied text nodes visible again | |
| // | |
| unhideTextNodesFromFragmentSource: function(nodes) { | |
| function visit(node) { | |
| var child, next; | |
| switch (node.nodeType) { | |
| case 3: // Text node | |
| // we have to remove their content the hard way... | |
| if("cssRegionsSavedNodeValue" in node) { | |
| node.nodeValue = node.cssRegionsSavedNodeValue; | |
| delete node.cssRegionsSavedNodeValue; | |
| } | |
| break; | |
| case 1: // Element node | |
| if(typeof(k)=="undefined") return; | |
| case 9: // Document node | |
| case 11: // Document fragment node | |
| child = node.firstChild; | |
| while (child) { | |
| next = child.nextSibling; | |
| visit(child); | |
| child = next; | |
| } | |
| break; | |
| } | |
| } | |
| nodes.forEach(visit); | |
| }, | |
| // | |
| // prepares the content elements to return to ther normal css life | |
| // | |
| unmarkNodesAsFragmentSource: function(nodes) { | |
| function visit(node,k) { | |
| var child, next; | |
| switch (node.nodeType) { | |
| case 3: // Text node | |
| // we have to reinstall their content the hard way... | |
| if("cssRegionsSavedNodeValue" in node) { | |
| node.nodeValue = node.cssRegionsSavedNodeValue; | |
| delete node.cssRegionsSavedNodeValue; | |
| } | |
| break; | |
| case 1: // Element node | |
| node.removeAttribute('data-css-regions-cloned'); | |
| node.removeAttribute('data-css-regions-fragment-source'); | |
| if(node.currentStyle) node.currentStyle.display.toString(); // IEFIX FOR BAD STYLE RECALC | |
| if(node.tagName=="OL") cssRegionsHelpers.unexpandListValues(node); | |
| if(typeof(k)!="undefined" && node.tagName=="LI") cssRegionsHelpers.unexpandListValues(node.parentNode); | |
| case 9: // Document node | |
| case 11: // Document fragment node | |
| child = node.firstChild; | |
| while (child) { | |
| next = child.nextSibling; | |
| visit(child); | |
| child = next; | |
| } | |
| break; | |
| } | |
| } | |
| nodes.forEach(visit); | |
| }, | |
| // | |
| // marks cloned content as fragment instead of as fragment source (basically) | |
| // | |
| transformFragmentSourceToFragments: function(nodes) { | |
| function visit(node) { | |
| var child, next; | |
| switch (node.nodeType) { | |
| case 1: // Element node | |
| var id = node.getAttribute('data-css-regions-fragment-source'); | |
| node.removeAttribute('data-css-regions-fragment-source'); | |
| node.removeAttribute('data-css-regions-cloning'); | |
| node.removeAttribute('data-css-regions-cloned'); | |
| node.setAttribute('data-css-regions-fragment-of', id); | |
| if(node.id) node.id += "--fragment"; | |
| case 9: // Document node | |
| case 11: // Document fragment node | |
| child = node.firstChild; | |
| while (child) { | |
| next = child.nextSibling; | |
| visit(child); | |
| child = next; | |
| } | |
| break; | |
| } | |
| } | |
| nodes.forEach(visit); | |
| }, | |
| // | |
| // removes some invisible text nodes from the tree | |
| // (useful if you don't want to face browser bugs when dealing with them) | |
| // | |
| embedTrailingWhiteSpaceNodes: function(fragment) { | |
| var onlyWhiteSpace = /^\s*$/; | |
| function visit(node) { | |
| var child, next; | |
| switch (node.nodeType) { | |
| case 3: // Text node | |
| // we only remove nodes at the edges | |
| if (!node.previousSibling) { | |
| // we only remove nodes if their parent doesn't preserve whitespace | |
| if (getComputedStyle(node.parentNode).whiteSpace.substring(0,3)!=="pre") { | |
| // only remove pure whitespace nodes | |
| if (onlyWhiteSpace.test(node.nodeValue)) { | |
| node.parentNode.setAttribute('data-whitespace-before',node.nodeValue); | |
| node.parentNode.removeChild(node); | |
| } | |
| } | |
| break; | |
| } | |
| // we only remove nodes at the edges | |
| if (!node.nextSibling) { | |
| // we only remove nodes if their parent doesn't preserve whitespace | |
| if (getComputedStyle(node.parentNode).whiteSpace.substring(0,3)!=="pre") { | |
| // only remove pure whitespace nodes | |
| if (onlyWhiteSpace.test(node.nodeValue)) { | |
| node.parentNode.setAttribute('data-whitespace-after',node.nodeValue); | |
| node.parentNode.removeChild(node); | |
| } | |
| } | |
| break; | |
| } | |
| break; | |
| case 1: // Element node | |
| case 9: // Document node | |
| case 11: // Document fragment node | |
| child = node.firstChild; | |
| while (child) { | |
| next = child.nextSibling; | |
| visit(child); | |
| child = next; | |
| } | |
| break; | |
| } | |
| } | |
| visit(fragment); | |
| }, | |
| // | |
| // recover the previously removed invisible text nodes | |
| // | |
| unembedTrailingWhiteSpaceNodes: function(fragment) { | |
| var onlyWhiteSpace = /^\s*$/; | |
| function visit(node) { | |
| var child, next; | |
| switch (node.nodeType) { | |
| case 1: // Element node | |
| var txt = ""; | |
| if(txt = node.getAttribute('data-whitespace-before')) { | |
| if(node.getAttribute('data-starting-fragment')=='' && node.getAttribute('data-special-starting-fragment','')) { | |
| node.insertBefore(document.createTextNode(txt),node.firstChild); | |
| } | |
| } | |
| node.removeAttribute('data-whitespace-before') | |
| if(txt = node.getAttribute('data-whitespace-after')) { | |
| if(node.getAttribute('data-continued-fragment')=='' && node.getAttribute('data-special-continued-fragment','')) { | |
| node.insertAfter(document.createTextNode(txt),node.lastChild); | |
| } | |
| } | |
| node.removeAttribute('data-whitespace-after') | |
| case 9: // Document node | |
| case 11: // Document fragment node | |
| child = node.firstChild; | |
| while (child) { | |
| next = child.nextSibling; | |
| visit(child); | |
| child = next; | |
| } | |
| break; | |
| } | |
| } | |
| visit(fragment); | |
| }, | |
| /// | |
| /// walk the two trees the same way, and copy all the styles | |
| /// BEWARE: if the DOMs are different, funny things will happen | |
| /// NOTE: this function will also remove elements put in another flow | |
| /// | |
| copyStyle: function(root1, root2) { | |
| function visit(node1, node2, isRoot) { | |
| var child1, next1, child2, next2; | |
| switch (node1.nodeType) { | |
| case 1: // Element node | |
| // firstly, setup a cache of all css properties on the element | |
| var matchedRules = (node1.currentStyle && !window.opera) ? undefined : cssCascade.findAllMatchingRules(node1) | |
| // and compute the value of all css properties | |
| var properties = cssCascade.allCSSProperties || cssCascade.getAllCSSProperties(); | |
| for(var p=properties.length; p--; ) { | |
| // if the property is computation-safe, use the computed value | |
| if(!(properties[p] in cssCascade.computationUnsafeProperties) && properties[p][0]!='-') { | |
| var style = getComputedStyle(node1).getPropertyValue(properties[p]); | |
| var defaultStyle = cssCascade.getDefaultStyleForTag(node1.tagName).getPropertyValue(properties[p]); | |
| if(style != defaultStyle) node2.style.setProperty(properties[p], style) | |
| continue; | |
| } | |
| // otherwise, get the element's specified value | |
| var cssValue = cssCascade.getSpecifiedStyle(node1, properties[p], matchedRules); | |
| if(cssValue && cssValue.length) { | |
| // if we have a specified value, let's use it | |
| node2.style.setProperty(properties[p], cssValue.toCSSString()); | |
| } else if(isRoot && node1.parentNode && properties[p][0] != '-') { | |
| // NOTE: the root will be detached from its parent | |
| // Therefore, we have to inherit styles from it (oh no!) | |
| // TODO: create a list of inherited properties | |
| if(!(properties[p] in cssCascade.inheritingProperties)) continue; | |
| // if the property is computation-safe, use the computed value | |
| if((properties[p]=="font-size") || (!(properties[p] in cssCascade.computationUnsafeProperties) && properties[p][0]!='-')) { | |
| var style = getComputedStyle(node1).getPropertyValue(properties[p]); | |
| node2.style.setProperty(properties[p], style); | |
| //var parentStyle = style; try { parentStyle = getComputedStyle(node1.parentNode).getPropertyValue(properties[p]) } catch(ex){} | |
| //var defaultStyle = cssCascade.getDefaultStyleForTag(node1.tagName).getPropertyValue(properties[p]); | |
| //if(style === parentStyle) { | |
| // node2.style.setProperty(properties[p], style) | |
| //} | |
| continue; | |
| } | |
| // otherwise, get the parent's specified value | |
| var cssValue = cssCascade.getSpecifiedStyle(node1, properties[p], matchedRules); | |
| if(cssValue && cssValue.length) { | |
| // if we have a specified value, let's use it | |
| node2.style.setProperty(properties[p], cssValue.toCSSString()); | |
| } | |
| } | |
| } | |
| // now, let's work on ::after and ::before | |
| var importPseudo = function(node1,node2,pseudo) { | |
| // | |
| // we'll need to use getSpecifiedStyle here as the pseudo thing is slow | |
| // | |
| var mayExist = !!cssCascade.findAllMatchingRulesWithPseudo(node1,pseudo.substr(1)).length; | |
| if(!mayExist) return; | |
| var pseudoStyle = getComputedStyle(node1,pseudo); | |
| if(pseudoStyle.content!='none'){ | |
| // let's create a stylesheet for the element | |
| var stylesheet = document.createElement('style'); | |
| stylesheet.setAttribute('data-no-css-polyfill',true); | |
| // compute the value of all css properties | |
| var node2style = ""; | |
| var properties = cssCascade.allCSSProperties || cssCascade.getAllCSSProperties(); | |
| for(var p=properties.length; p--; ) { | |
| // we always use the computed value, because we don't have better | |
| var style = pseudoStyle.getPropertyValue(properties[p]); | |
| node2style += properties[p]+":"+style+";"; | |
| } | |
| stylesheet.textContent = ( | |
| '[data-css-regions-fragment-of="' + node1.getAttribute('data-css-regions-fragment-source') + '"]' | |
| +':not([data-css-regions-starting-fragment]):not([data-css-regions-special-starting-fragment])' | |
| +':'+pseudo+'{' | |
| +node2style | |
| +"}" | |
| ); | |
| node2.parentNode.insertBefore(stylesheet, node2); | |
| } | |
| } | |
| importPseudo(node1,node2,":before"); | |
| importPseudo(node1,node2,":after"); | |
| // retarget events | |
| cssRegionsHelpers.retargetEvents(node1,node2); | |
| case 9: // Document node | |
| case 11: // Document fragment node | |
| child1 = node1.firstChild; | |
| child2 = node2.firstChild; | |
| while (child1) { | |
| next1 = child1.nextSibling; | |
| next2 = child2.nextSibling; | |
| // decide between process style or hide | |
| if(child1.cssRegionsLastFlowIntoName && child1.cssRegionsLastFlowIntoType==="element") { | |
| node2.removeChild(child2); | |
| } else { | |
| visit(child1, child2); | |
| } | |
| child1 = next1; | |
| child2 = next2; | |
| } | |
| break; | |
| } | |
| } | |
| visit(root1, root2, true); | |
| }, | |
| // | |
| // make sure the most critical events still fire in the fragment source | |
| // even if the browser initially fire them on the fragments | |
| // | |
| retargetEvents: function retargetEvents(node1,node2) { | |
| var retargetEvent = "cssRegionsHelpers.retargetEvent(this,event)"; | |
| node2.setAttribute("onclick", retargetEvent); | |
| node2.setAttribute("ondblclick", retargetEvent); | |
| node2.setAttribute("onmousedown", retargetEvent); | |
| node2.setAttribute("onmouseup", retargetEvent); | |
| node2.setAttribute("onmousein", retargetEvent); | |
| node2.setAttribute("onmouseout", retargetEvent); | |
| node2.setAttribute("onmouseenter", retargetEvent); | |
| node2.setAttribute("onmouseleave", retargetEvent); | |
| }, | |
| // | |
| // single hub for event retargeting operations. | |
| // | |
| retargetEvent: function retargeEvent(node2,e) { | |
| // get the node we should fire the event on | |
| var node1 = ( | |
| (node2.cssRegionsFragmentSource) || | |
| (node2.cssRegionsFragmentSource=document.querySelector('[data-css-regions-fragment-source="' + node2.getAttribute('data-css-regions-fragment-of') + '"]')) | |
| ); | |
| if(node1) { | |
| // dispatch the event on the real node | |
| var ne = domEvents.cloneEvent(e); | |
| node1.dispatchEvent(ne); | |
| // prevent the event to fire on the region | |
| e.stopImmediatePropagation ? e.stopImmediatePropagation() : e.stopPropagation(); | |
| // make sure to cancel the event if required | |
| if(ne.isDefaultPrevented || ne.defaultPrevented) { e.preventDefault(); return false; } | |
| } | |
| } | |
| }; | |
| return cssRegionsHelpers; | |
| })(window, document); | |
| require.define('src/css-regions/lib/helpers.js'); | |
| //////////////////////////////////////// | |
| // | |
| // this module holds the front-facing features of the polyfill | |
| // | |
| module.exports = (function(window, document, cssRegions) { "use strict"; | |
| var domEvents = require('src/core/dom-events.js'); | |
| var cssSyntax = require('src/core/css-syntax.js'); | |
| var cssCascade = require('src/core/css-cascade.js'); | |
| var cssBreak = require('src/core/css-break.js'); | |
| var cssRegionsHelpers = require('src/css-regions/lib/helpers.js'); | |
| var ES = require('src/core/dom-experimental-event-streams.js'); | |
| // | |
| // this class contains flow-relative data field | |
| // | |
| cssRegions.Flow = function NamedFlow(name) { | |
| // TODO: force immediate relayout if someone ask the overset properties | |
| // and the layout has been deemed wrong (still isn't a proof of correctness but okay) | |
| // define the flow name | |
| this.name = name; Object.defineProperty(this, "name", {get: function() { return name; }}); | |
| // define the overset status | |
| this.overset = false; | |
| // define the first empty region | |
| this.firstEmptyRegionIndex = -1; | |
| // elements poured into the flow | |
| this.content = []; this.lastContent = []; | |
| // elements that consume this flow | |
| this.regions = []; this.lastRegions = []; | |
| // event handlers | |
| this.eventListeners = { | |
| "regionfragmentchange": [], | |
| "regionoversetchange": [], | |
| }; | |
| // this function is used to work with dom event streams | |
| var This=this; This.update = function(stream) { | |
| stream.schedule(This.update); This.relayout(); | |
| }; | |
| // register to style changes already | |
| This.lastStylesheetAdded = 0; | |
| cssCascade.addEventListener('stylesheetadded', function() { | |
| if(This.lastStylesheetAdded - Date() > 100) { | |
| This.lastStylesheetAdded = +Date(); | |
| This.relayout(); | |
| } else { | |
| cssConsole.warn("Please don't add stylesheets as a response to region events. Operation cancelled.") | |
| } | |
| }); | |
| // a small counter to avoid enter retry loops | |
| This.failedLayoutCount = 0; | |
| // some other fields | |
| This.lastEventRAF = 0; | |
| This.restartLayout = false; | |
| } | |
| cssRegions.Flow.prototype.removeFromContent = function(element) { | |
| // clean up stuff | |
| this.removeEventListenersOf(element); | |
| // remove reference | |
| var index = this.content.indexOf(element); | |
| if(index>=0) { this.content.splice(index,1); } | |
| }; | |
| cssRegions.Flow.prototype.removeFromRegions = function(element) { | |
| // clean up stuff | |
| this.removeEventListenersOf(element); | |
| // remove reference | |
| var index = this.regions.indexOf(element); | |
| if(index>=0) { this.regions.splice(index,1); } | |
| }; | |
| cssRegions.Flow.prototype.addToContent = function(element) { | |
| // walk the tree to find an element inside the content chain | |
| var content = this.content; | |
| var treeWalker = document.createTreeWalker( | |
| document.documentElement, | |
| NodeFilter.SHOW_ELEMENT, | |
| function(node) { | |
| return content.indexOf(node) >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; | |
| }, | |
| false | |
| ); | |
| // which by the way has to be after the considered element | |
| treeWalker.currentNode = element; | |
| // if we find such node | |
| if(treeWalker.nextNode()) { | |
| // insert the element at his current location | |
| content.splice(content.indexOf(treeWalker.currentNode),0,element); | |
| } else { | |
| // add the new element to the end of the array | |
| content.push(element); | |
| } | |
| }; | |
| cssRegions.Flow.prototype.addToRegions = function(element) { | |
| // walk the tree to find an element inside the region chain | |
| var regions = this.regions; | |
| var treeWalker = document.createTreeWalker( | |
| document.documentElement, | |
| NodeFilter.SHOW_ELEMENT, | |
| function(node) { | |
| return regions.indexOf(node) >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; | |
| }, | |
| false | |
| ); | |
| // which by the way has to be after the considered element | |
| treeWalker.currentNode = element; | |
| // if we find such node | |
| if(treeWalker.nextNode()) { | |
| // insert the element at his current location | |
| regions.splice(this.regions.indexOf(treeWalker.currentNode),0,element); | |
| } else { | |
| // add the new element to the end of the array | |
| regions.push(element); | |
| } | |
| }; | |
| cssRegions.Flow.prototype.generateContentFragment = function() { | |
| var fragment = document.createDocumentFragment(); var This=this; | |
| // add copies of all due content | |
| for(var i=0; i<this.content.length; i++) { | |
| var element = this.content[i]; | |
| // | |
| // STEP 1: IDENTIFY FRAGMENT SOURCES AS SUCH | |
| // | |
| cssRegionsHelpers.markNodesAsFragmentSource([element], element.cssRegionsLastFlowIntoType=="content"); | |
| // | |
| // STEP 2: CLONE THE FRAGMENT SOURCES | |
| // | |
| // depending on the requested behavior | |
| if(element.cssRegionsLastFlowIntoType=="element") { | |
| // add the element | |
| var el = element; | |
| var elClone = el.cloneNode(true); | |
| var elToInsert = elClone; if(elToInsert.tagName=="LI") { | |
| elToInsert = document.createElement(el.parentNode.tagName); | |
| elToInsert.style.margin="0"; | |
| elToInsert.style.padding="0"; | |
| elToInsert.appendChild(elClone); | |
| } | |
| fragment.appendChild(elToInsert); | |
| // clone the style | |
| cssRegionsHelpers.copyStyle(el, elClone); | |
| } else { | |
| // add current children | |
| var el = element.firstChild; while(el) { | |
| // add the element | |
| var elClone = el.cloneNode(true); | |
| var elToInsert = elClone; if(elToInsert.tagName=="LI") { | |
| elToInsert = document.createElement(el.parentNode.tagName); | |
| elToInsert.style.margin="0"; | |
| elToInsert.style.padding="0"; | |
| elToInsert.appendChild(elClone); | |
| } | |
| fragment.appendChild(elToInsert); | |
| // clone the style | |
| cssRegionsHelpers.copyStyle(el, elClone); | |
| el = el.nextSibling; | |
| } | |
| } | |
| } | |
| // | |
| // STEP 3: HIDE TEXT NODES IN FRAGMENT SOURCES | |
| // | |
| cssRegionsHelpers.hideTextNodesFromFragmentSource(this.content); | |
| // | |
| // STEP 4: CONVERT CLONED FRAGMENT SOURCES INTO TRUE FRAGMENTS | |
| // | |
| cssRegionsHelpers.transformFragmentSourceToFragments( | |
| Array.prototype.slice.call(fragment.childNodes,0) | |
| ) | |
| // | |
| // CLONED CONTENT IS READY! | |
| // | |
| return fragment; | |
| } | |
| cssRegions.Flow.prototype.relayout = function() { | |
| var This = this; | |
| // prevent previous relayouts from eventing | |
| cancelAnimationFrame(This.lastEventRAF); | |
| // batch relayout queries | |
| if(This.relayoutScheduled) { return; } | |
| if(This.relayoutInProgress) { This.restartLayout=true; return; } | |
| This.relayoutScheduled = true; | |
| requestAnimationFrame(function() { This._relayout() }); | |
| } | |
| cssRegions.Flow.prototype._relayout = function(data){ | |
| var This=this; | |
| try { | |
| // | |
| // note: it is recommended to look at the beautiful | |
| // drawings I made before attempting to understand | |
| // this stuff. If you don't have them, ask me. | |
| // | |
| cssConsole.log("starting a new relayout for "+This.name); | |
| This.relayoutInProgress=true; This.relayoutScheduled=false; | |
| This.lastRelayout = +new Date(); | |
| //debugger; | |
| // NOTE: we recover the scroll position in case the browser mess it up | |
| var docElmScrollTop = data && data.docElmScrollTop ? data.docElmScrollTop : document.documentElement.scrollTop; | |
| var docBdyScrollTop = data && data.docBdyScrollTop ? data.docBdyScrollTop : document.body.scrollTop; | |
| // | |
| // STEP 1: REMOVE PREVIOUS EVENT LISTENERS | |
| // | |
| // remove the listeners from everything | |
| This.removeEventListenersOf(This.lastRegions); | |
| This.removeEventListenersOf(This.lastContent); | |
| cancelAnimationFrame(This.lastEventRAF); | |
| // | |
| // STEP 2: RESTORE CONTENT/REGIONS TO A CLEAN STATE | |
| // | |
| // detect elements being removed of the document | |
| This.regions = This.regions.filter(function(e) { return document.documentElement.contains(e); }) | |
| This.content = This.content.filter(function(e) { return document.documentElement.contains(e); }) | |
| // cleanup previous layout | |
| cssRegionsHelpers.unmarkNodesAsRegion(This.lastRegions); This.lastRegions = This.regions.slice(0); | |
| cssRegionsHelpers.unmarkNodesAsFragmentSource(This.lastContent); This.lastContent = This.content.slice(0); | |
| // | |
| // STEP 3: EMPTY ALL REGIONS | |
| // ADD WRAPPER FOR FLOW CONTENT | |
| // PREPARE FOR CONTENT CLONING | |
| // | |
| // empty all the regions | |
| cssRegionsHelpers.markNodesAsRegion(This.regions); | |
| // create a fresh list of the regions | |
| var regionStack = This.regions.slice(0).reverse(); | |
| // | |
| // STEP 4: CLONE THE CONTENT | |
| // ADD METADATA TO CLONED CONTENT | |
| // HIDE FLOW CONTENT AT INITIAL POSITION | |
| // | |
| // create a fresh list of the content | |
| // compute the style of all source elements | |
| // generate stylesheets for those rules | |
| var contentFragment = This.generateContentFragment(); | |
| // | |
| // STEP 5: POUR CONTENT INTO THE REGIONS | |
| // | |
| // layout this stuff | |
| cssRegions.layoutContent(regionStack, contentFragment, { | |
| onprogress: function(continueLayout) { | |
| // NOTE: we recover the scroll position in case the browser mess it up | |
| document.documentElement.scrollTop = docElmScrollTop; | |
| document.body.scrollTop = docBdyScrollTop; | |
| // NOTE: if the current layout goes nowhere, start a new one already | |
| if(This.restartLayout) { | |
| This.relayoutInProgress = false; | |
| This.failedLayoutCount = 0; | |
| This.restartLayout = false; | |
| This._relayout({ | |
| docElmScrollTop: docElmScrollTop, | |
| docBdyScrollTop: docBdyScrollTop | |
| }); | |
| } else { | |
| setImmediate(continueLayout); | |
| } | |
| }, | |
| ondone: function onLayoutDone(overset) { | |
| This.overset = overset; | |
| This.firstEmptyRegionIndex = This.regions.length-1; while(This.regions[This.firstEmptyRegionIndex]) { | |
| // tell whether the region is empty | |
| var isEmpty = false; | |
| isEmpty = isEmpty || !This.regions[This.firstEmptyRegionIndex].cssRegionsWrapper; | |
| isEmpty = isEmpty || !This.regions[This.firstEmptyRegionIndex].cssRegionsWrapper.firstChild; | |
| // if the region is not empty | |
| if(!isEmpty) { | |
| // the first empty region if the next one, if it exists | |
| if((++This.firstEmptyRegionIndex)==This.regions.length) { | |
| This.firstEmptyRegionIndex = -1; | |
| } | |
| break; | |
| } else { | |
| // else, let's try the previous region | |
| This.firstEmptyRegionIndex--; | |
| } | |
| } | |
| // | |
| // STEP 6: REGISTER TO UPDATE EVENTS | |
| // | |
| // make sure regions update are taken in consideration | |
| if(window.MutationObserver) { | |
| This.addEventListenersTo(This.content); | |
| This.addEventListenersTo(This.regions); | |
| } else { | |
| // the other browsers don't get this as acurately | |
| // but that shouldn't be that of an issue for 99% of the cases | |
| setImmediate(function() { | |
| This.addEventListenersTo(This.content); | |
| }); | |
| } | |
| // | |
| // STEP 7: FIRE SOME EVENTS | |
| // | |
| if(This.regions.length > 0 && !This.restartLayout) { | |
| // before doing anything, let's check our stuff is consistent | |
| var isBuggy = false; | |
| isBuggy = isBuggy || This.regions.some(function(e) { return !document.documentElement.contains(e); }) | |
| isBuggy = isBuggy || This.content.some(function(e) { return !document.documentElement.contains(e); }) | |
| if(isBuggy) { | |
| // if we found any bug, we will need to restart a layout | |
| cssConsole.warn("Buggy css regions layout: the page changed; we need to restart."); | |
| This.restartLayout = true; | |
| } else { | |
| // if it was okay, let's fire some event | |
| This.lastEventRAF = requestAnimationFrame(function() { | |
| // TODO: only fire when necessary but... | |
| This.dispatchEvent('regionfragmentchange'); | |
| This.dispatchEvent('regionoversetchange'); | |
| }); | |
| } | |
| } | |
| // NOTE: we recover the scroll position in case the browser mess it up | |
| document.documentElement.scrollTop = docElmScrollTop; | |
| document.body.scrollTop = docBdyScrollTop; | |
| // mark layout has being done | |
| This.relayoutInProgress = false; | |
| This.failedLayoutCount = 0; | |
| // restart a layout if a request was queued during the current one | |
| if(This.restartLayout) { | |
| This.restartLayout = false; | |
| This.relayout(); | |
| } | |
| } | |
| }); | |
| } catch(ex) { | |
| // sometimes IE fails for no valid reason | |
| // (other than the page is still loading) | |
| setImmediate(function() { throw ex; }); | |
| // but we cannot accept to fail, so we need to try again | |
| // until we finish a complete layout pass... | |
| This.failedLayoutCount++; | |
| if(This.failedLayoutCount<7) {requestAnimationFrame(function() { This._relayout() });} | |
| else {This.failedLayoutCount=0; This.relayoutScheduled=false; This.relayoutInProgress=false; This.restartLayout=false; } | |
| } | |
| } | |
| cssRegions.Flow.prototype.relayoutIfSizeChanged = function() { | |
| // go through all regions | |
| // and see if any did change of size | |
| var rs = this.regions; | |
| for(var i=rs.length; i--; ) { | |
| if( | |
| rs[i].offsetHeight !== rs[i].cssRegionsLastOffsetHeight | |
| || rs[i].offsetWidth !== rs[i].cssRegionsLastOffsetWidth | |
| ) { | |
| this.relayout(); return; | |
| } | |
| } | |
| } | |
| cssRegions.Flow.prototype.addEventListenersTo = function(nodes) { | |
| var This=this; if(nodes instanceof Element) { nodes=[nodes] } | |
| nodes.forEach(function(element) { | |
| if(!element.cssRegionsEventStream) { | |
| element.cssRegionsEventStream = new ES.DOMUpdateEventStream({target: element}); | |
| element.cssRegionsEventStream.schedule(This.update); | |
| } | |
| }); | |
| } | |
| cssRegions.Flow.prototype.removeEventListenersOf = function(nodes) { | |
| var This=this; if(nodes instanceof Element) { nodes=[nodes] } | |
| nodes.forEach(function(element) { | |
| if(element.cssRegionsEventStream) { | |
| element.cssRegionsEventStream.dispose(); | |
| delete element.cssRegionsEventStream; | |
| } | |
| }); | |
| } | |
| // alias | |
| cssRegions.NamedFlow = cssRegions.Flow; | |
| // return a disconnected array of the content of a NamedFlow | |
| cssRegions.NamedFlow.prototype.getContent = function getContent() { | |
| return this.content.slice(0) | |
| } | |
| // return a disconnected array of the regions of a NamedFlow | |
| cssRegions.NamedFlow.prototype.getRegions = function getRegions() { | |
| return this.regions.slice(0) | |
| } | |
| cssRegions.NamedFlow.prototype.getRegionsByContent = function getRegionsByContent(node) { | |
| var regions = []; | |
| var fragments = document.querySelectorAll('[data-css-regions-fragment-of="'+node.getAttribute('data-css-regions-fragment-source')+'"]'); | |
| for (var i=0; i<fragments.length; i++) { | |
| var current=fragments[i]; do { | |
| if(current.getAttribute('data-css-region')) { | |
| regions.push(current); break; | |
| } | |
| } while(current=current.parentNode); | |
| } | |
| return regions; | |
| } | |
| domEvents.EventTarget.implementsIn(cssRegions.Flow); | |
| // | |
| // this class is a collection of named flows (not an array, sadly) | |
| // | |
| cssRegions.NamedFlowCollection = function NamedFlowCollection() { | |
| this.length = 0; | |
| } | |
| cssRegions.NamedFlowCollection.prototype.namedItem = function(k) { | |
| return cssRegions.flows[k] || (cssRegions.flows[k]=new cssRegions.Flow(k)); | |
| } | |
| // | |
| // this helper creates the required methods on top of the DOM {ie: public exports} | |
| // | |
| cssRegions.enablePolyfillObjectModel = function() { | |
| // | |
| // DOCUMENT INTERFACE | |
| // | |
| // | |
| // returns a static list of active named flows | |
| // | |
| document.getNamedFlows = function() { | |
| var c = new cssRegions.NamedFlowCollection(); var flows = cssRegions.flows; | |
| for(var flowName in cssRegions.flows) { | |
| if(Object.prototype.hasOwnProperty.call(flows, flowName)) { | |
| // only active flows can be included | |
| if(flows[flowName].content.length!=0 || flows[flowName].regions.length!=0) { | |
| c[c.length++] = c[flowName] = flows[flowName]; | |
| } | |
| } | |
| } | |
| return c; | |
| } | |
| // | |
| // returns a live object for any named flow | |
| // | |
| document.getNamedFlow = function(flowName) { | |
| var flows = cssRegions.flows; | |
| return (flows[flowName] || (flows[flowName]=new cssRegions.NamedFlow(flowName))); | |
| } | |
| // | |
| // ELEMENT INTERFACE | |
| // | |
| Object.defineProperties( | |
| Element.prototype, | |
| { | |
| "regionOverset": { | |
| get: function() { | |
| return this._regionOverset || 'fit'; | |
| }, | |
| set: function(value) { | |
| this._regionOverset = value; | |
| } | |
| }, | |
| "getRegionFlowRanges": { | |
| value: function getRegionFlowRanges() { | |
| return null; // TODO: can we implement that? I think we can't (properly). | |
| } | |
| }, | |
| "getComputedRegionStyle": { | |
| value: function getComputedRegionStyle(element,pseudo) { | |
| // TODO: only works while we don't relayout | |
| // TODO: only works properly for elements actually in the region | |
| var fragment = document.querySelector('[data-css-regions-fragment-of="'+element.getAttribute('data-css-regions-fragment-source')+'"]'); | |
| if(pseudo) { | |
| return getComputedStyle(fragment||element, pseudo); | |
| } else { | |
| return getComputedStyle(fragment||element); | |
| } | |
| } | |
| } | |
| } | |
| ) | |
| // | |
| // CSSStyleDeclaration interface | |
| // | |
| cssCascade.polyfillStyleInterface('flow-into'); | |
| cssCascade.polyfillStyleInterface('flow-from'); | |
| cssCascade.polyfillStyleInterface('region-fragment'); | |
| cssCascade.polyfillStyleInterface('break-before'); | |
| cssCascade.polyfillStyleInterface('break-after'); | |
| } | |
| // load the polyfill immediately if not especially told otherwise | |
| if(!("cssRegionsManualTrigger" in window)) { cssRegions.enablePolyfill(); } | |
| }); | |
| require.define('src/css-regions/lib/objectmodel.js'); | |
| //////////////////////////////////////// | |
| // | |
| // this module holds the big-picture actions of the polyfill | |
| // | |
| module.exports = (function(window, document) { "use strict"; | |
| var domEvents = require('src/core/dom-events.js'); | |
| var cssSyntax = require('src/core/css-syntax.js'); | |
| var cssCascade = require('src/core/css-cascade.js'); | |
| var cssBreak = require('src/core/css-break.js'); | |
| require('src/css-regions/lib/range-extensions.js'); | |
| var cssRegionsHelpers = require('src/css-regions/lib/helpers.js'); | |
| var enableObjectModel = require('src/css-regions/lib/objectmodel.js'); | |
| var CSS_STYLE = "cssregion,[data-css-region]>*,[data-css-regions-fragment-source]:not([data-css-regions-cloning]),[data-css-regions-fragment-source][data-css-regions-cloned]{display:none!important}[data-css-region]>cssregion:last-of-type{display:inline!important}[data-css-region]{content:normal!important}[data-css-special-continued-fragment]{counter-reset:none!important;counter-increment:none!important;margin-bottom:0!important;border-bottom-left-radius:0!important;border-bottom-right-radius:0!important}[data-css-continued-fragment]{counter-reset:none!important;counter-increment:none!important;margin-bottom:0!important;padding-bottom:0!important;border-bottom:none!important;border-bottom-left-radius:0!important;border-bottom-right-radius:0!important}[data-css-continued-fragment]::after{content:none!important;display:none!important}[data-css-special-starting-fragment]{text-indent:0!important;margin-top:0!important}[data-css-starting-fragment]{text-indent:0!important;margin-top:0!important;padding-top:0!important;border-top:none!important;border-top-left-radius:0!important;border-top-right-radius:0!important}[data-css-starting-fragment]::before{content:none!important;display:none!important}[data-css-continued-block-fragment][data-css-continued-fragment]:not(:empty)::after{content:''!important;display:inline-block!important;width:100%!important;height:0!important;font-size:0!important;line-height:0!important;margin:0!important;padding:0!important;border:0!important}"; | |
| var cssRegions = { | |
| // | |
| // this function is at the heart of the region polyfill | |
| // it will iteratively fill a list of regions until no | |
| // content or no region is left | |
| // | |
| // the before-overflow size of a region is determined by | |
| // adding all content to it and comparing his offsetHeight | |
| // and his scrollHeight | |
| // | |
| // when this is done, we use dom ranges to detect the point | |
| // where the content exceed this box and we split the fragment | |
| // at that point. | |
| // | |
| // when splitting inside an element, the borders, paddings and | |
| // generated content must be tied to the right fragments which | |
| // require some code | |
| // | |
| // this functions returns whether some content was still remaining | |
| // when the flow when the last region was filled. please not this | |
| // can only happen if this last region has "region-fragment" set | |
| // to break, otherwhise all the content will automatically overflow | |
| // this last region. | |
| // | |
| layoutContent: function(regions, remainingContent, callback, startTime) { | |
| // | |
| // this function will iteratively fill all the regions | |
| // when we reach the last region, we return the overset status | |
| // | |
| // validate args | |
| if(!regions) return callback.ondone(!!remainingContent.hasChildNodes()); | |
| if(!regions.length) return callback.ondone(!!remainingContent.hasChildNodes()); | |
| if(!startTime) startTime = Date.now(); | |
| // get the next region | |
| var region = regions.pop(); | |
| // NOTE: while we don't monitor that, and it can therefore become inaccurate | |
| // I'm going to follow the spec and refuse to mark as region inline/none elements] | |
| while(true) { | |
| var regionDisplay = getComputedStyle(region).display; | |
| if(regionDisplay == "none" || regionDisplay.indexOf("inline") !== -1) { | |
| if(region = regions.pop()) { continue } else { return callback.ondone(!!remainingContent.hasChildNodes()) }; | |
| } else { | |
| break; | |
| } | |
| } | |
| // the polyfill actually use a <cssregion> wrapper | |
| // we need to link this wrapper and the actual region | |
| if(region.cssRegionsWrapper) { | |
| region.cssRegionsWrapper.cssRegionHost = region; | |
| region = region.cssRegionsWrapper; | |
| } else { | |
| region.cssRegionHost = region; | |
| } | |
| // empty the region | |
| region.innerHTML = ''; | |
| // avoid doing the layout of empty regions | |
| if(!remainingContent.hasChildNodes()) { | |
| region.cssRegionHost.cssRegionsLastOffsetHeight = region.cssRegionHost.offsetHeight; | |
| region.cssRegionHost.cssRegionsLastOffsetWidth = region.cssRegionHost.offsetWidth; | |
| region.cssRegionHost.regionOverset = 'empty'; | |
| var dummyCallback = { ondone:function(){}, onprogress:function(f){f()} }; | |
| cssRegions.layoutContent(regions, remainingContent, dummyCallback, startTime); | |
| return callback.ondone(false); | |
| } | |
| // append the remaining content to the region | |
| region.appendChild(remainingContent); | |
| // check if we have more regions to process | |
| if(regions.length !== 0) { | |
| return this.layoutContentInNextRegionsWhenReady(region, regions, remainingContent, callback, startTime); | |
| } else { | |
| return this.layoutContentInLastRegionWhenReady(region, regions, remainingContent, callback, startTime); | |
| } | |
| }, | |
| layoutContentInNextRegionsWhenReady: function(region, regions, remainingContent, callback, startTime) { | |
| // delays until all images are loaded | |
| var imgs = region.getElementsByTagName('img'); | |
| for(var imgs_index=imgs.length; imgs_index--; ) { | |
| if(!imgs[imgs_index].complete && !imgs[imgs_index].hasAttribute('height')) { | |
| return setTimeout( | |
| function() { | |
| this.layoutContentInNextRegionsWhenReady(region, regions, remainingContent, callback, startTime+32); | |
| }.bind(this), | |
| 16 | |
| ); | |
| } | |
| } | |
| // check if there was an overflow or some break-before/after instruction | |
| var regionDidOverflow = region.cssRegionHost.scrollHeight != region.cssRegionHost.offsetHeight; | |
| var shouldSegmentContent = regionDidOverflow; | |
| if(!shouldSegmentContent) { | |
| var first = region.firstElementChild; | |
| var last = region.lastElementChild; | |
| var current = first; | |
| while(current) { | |
| if(current != first) { | |
| if(/(region|all|always)/i.test(cssCascade.getSpecifiedStyle(current,'break-before',undefined,true).toCSSString())) { | |
| shouldSegmentContent = true; break; | |
| } | |
| } | |
| if(current != last) { | |
| if(/(region|all|always)/i.test(cssCascade.getSpecifiedStyle(current,'break-after',undefined,true).toCSSString())) { | |
| current = current.nextElementSibling; | |
| shouldSegmentContent = true; break; | |
| } | |
| } | |
| current = current.nextElementSibling; | |
| } | |
| } | |
| if(shouldSegmentContent) { | |
| // the remaining content is what was overflowing | |
| remainingContent = this.extractOverflowingContent(region); | |
| } else { | |
| // there's nothing more to insert | |
| remainingContent = document.createDocumentFragment(); | |
| } | |
| // if any content didn't fit | |
| if(remainingContent.hasChildNodes()) { | |
| region.cssRegionHost.regionOverset = 'overset'; | |
| } else { | |
| region.cssRegionHost.regionOverset = 'fit'; | |
| } | |
| // update flags | |
| region.cssRegionHost.cssRegionsLastOffsetHeight = region.cssRegionHost.offsetHeight; | |
| region.cssRegionHost.cssRegionsLastOffsetWidth = region.cssRegionHost.offsetWidth; | |
| // layout the next regions | |
| // WE LET THE NEXT REGION DECIDE WHAT TO RETURN | |
| if(startTime+200 > Date.now()) { | |
| return cssRegions.layoutContent(regions, remainingContent, callback, startTime); | |
| } else { | |
| return callback.onprogress(function() { | |
| cssRegions.layoutContent(regions, remainingContent, callback); | |
| }); | |
| } | |
| }, | |
| layoutContentInLastRegionWhenReady: function(region, regions, remainingContent, callback, startTime) { | |
| // delays until all images are loaded | |
| var imgs = region.getElementsByTagName('img'); | |
| for(var imgs_index=imgs.length; imgs_index--; ) { | |
| if(!imgs[imgs_index].complete && !imgs[imgs_index].hasAttribute('height')) { | |
| return setTimeout( | |
| function() { | |
| this.layoutContentInLastRegionWhenReady(region, regions, remainingContent, callback, startTime+32); | |
| }.bind(this), | |
| 32 | |
| ); | |
| } | |
| } | |
| // support region-fragment: break | |
| if(cssCascade.getSpecifiedStyle(region.cssRegionHost,"region-fragment",undefined,true).toCSSString().trim().toLowerCase()=="break") { | |
| // WE RETURN TRUE IF WE DID OVERFLOW | |
| var didOverflow = (this.extractOverflowingContent(region).hasChildNodes()); | |
| // update flags | |
| region.cssRegionHost.cssRegionsLastOffsetHeight = region.cssRegionHost.offsetHeight; | |
| region.cssRegionHost.cssRegionsLastOffsetWidth = region.cssRegionHost.offsetWidth; | |
| return callback.ondone(didOverflow); | |
| } else { | |
| // update flags | |
| region.cssRegionHost.cssRegionsLastOffsetHeight = region.cssRegionHost.offsetHeight; | |
| region.cssRegionHost.cssRegionsLastOffsetWidth = region.cssRegionHost.offsetWidth; | |
| // WE RETURN FALSE IF WE DIDN'T OVERFLOW | |
| return callback.ondone(region.cssRegionHost.offsetHeight != region.cssRegionHost.scrollHeight); | |
| } | |
| }, | |
| // | |
| // this function returns a document fragment containing the content | |
| // that didn't fit in a particular <cssregion> element. | |
| // | |
| // in the simplest cases, we can just use hit-targeting to get very | |
| // close the the natural breaking point. for mostly textual flows, | |
| // this works perfectly, for the others, we may need some tweaks. | |
| // | |
| // there's a code detecting whether this hit-target optimization | |
| // did possibly fail, in which case we return to a setup where we | |
| // start from scratch. | |
| // | |
| extractOverflowingContent: function(region, dontOptimize) { | |
| // make sure empty nodes don't make our life more difficult | |
| cssRegionsHelpers.embedTrailingWhiteSpaceNodes(region); | |
| // get the region layout | |
| var sizingH = region.cssRegionHost.offsetHeight; // avail size (max-height) | |
| var sizingW = region.cssRegionHost.offsetWidth; // avail size (max-width) | |
| var pos = region.cssRegionHost.getBoundingClientRect(); // avail size? | |
| pos = {top: pos.top, bottom: pos.bottom, left: pos.left, right: pos.right}; | |
| // substract from the bottom any border/padding of the region | |
| var lostHeight = parseInt(getComputedStyle(region.cssRegionHost).paddingBottom); | |
| lostHeight += parseInt(getComputedStyle(region.cssRegionHost).borderBottomWidth); | |
| pos.bottom -= lostHeight; sizingH -= lostHeight; | |
| // | |
| // note: let's use hit targeting to find a dom range | |
| // which is close to the location where we will need to | |
| // break the content into fragments | |
| // | |
| // get the caret range for the bottom-right of that location | |
| try { | |
| var r = dontOptimize ? document.createRange() : document.caretRangeFromPoint( | |
| pos.left + sizingW - 1, | |
| pos.top + sizingH - 1 | |
| ); | |
| } catch (ex) { | |
| try { | |
| cssConsole.error(ex.message); | |
| cssConsole.dir(ex); | |
| } catch (ex) {} | |
| } | |
| // helper for logging info | |
| /*cssConsole.log("extracting overflow") | |
| cssConsole.log(pos.bottom)*/ | |
| var debug = function() { | |
| /*cssConsole.dir({ | |
| startContainer: r.startContainer, | |
| startOffset: r.startOffset, | |
| browserBCR: r.getBoundingClientRect(), | |
| computedBCR: rect | |
| });*/ | |
| } | |
| var fixNullRect = function() { | |
| if(rect.bottom==0 && rect.top==0 && rect.left==0 && rect.right==0) { | |
| var scrollTop = -(document.documentElement.scrollTop || document.body.scrollTop); | |
| var scrollLeft = -(document.documentElement.scrollLeft || document.body.scrollLeft); | |
| rect = { | |
| width: 0, | |
| heigth: 0, | |
| top: scrollTop, | |
| bottom: scrollTop, | |
| left: scrollLeft, | |
| right: scrollLeft | |
| } | |
| } | |
| } | |
| // if the caret is outside the region | |
| if(!r || (region !== r.endContainer && !Node.contains(region,r.endContainer))) { | |
| // if the caret is after the region wrapper but inside the host... | |
| if(r && r.endContainer === region.cssRegionHost && r.endOffset==r.endContainer.childNodes.length) { | |
| // move back at the end of the region, actually | |
| r.setStart(region, region.childNodes.length); | |
| r.setEnd(region, region.childNodes.length); | |
| } else { | |
| // move back into the region | |
| r = r || document.createRange(); | |
| r.setStart(region, 0); | |
| r.setEnd(region, 0); | |
| dontOptimize=true; | |
| } | |
| } | |
| // start finding the natural breaking point | |
| do { | |
| // store the current selection rect for fast access | |
| var rect = r.myGetExtensionRect(); fixNullRect(); | |
| debug(); | |
| // | |
| // note: maybe the text is right-to-left | |
| // in this case, we can go further than the caret | |
| // | |
| // move the end point char by char until it's completely in the region | |
| while(!(r.endContainer==region && r.endOffset==r.endContainer.childNodes.length) && rect.bottom<=pos.top+sizingH) { | |
| debug(); | |
| // look if we can optimize by moving fast forward | |
| var nextSibling = r.endContainer.childNodes[r.endOffset]; | |
| var nextSiblingRect = !nextSibling || Node.getBoundingClientRect(nextSibling); | |
| if(nextSibling && nextSiblingRect.bottom<=pos.top+sizingH) { | |
| // if yes, move element by element | |
| r.setStartAfter(nextSibling) | |
| r.setEndAfter(nextSibling) | |
| rect = nextSiblingRect | |
| fixNullRect() | |
| } else { | |
| // otherwise, go char-by-char | |
| r.myMoveTowardRight(); rect = r.myGetExtensionRect(); fixNullRect(); | |
| } | |
| } | |
| // | |
| // note: maybe the text is one line too big | |
| // in this case, we have to backtrack a little | |
| // | |
| // move the end point char by char until it's completely in the region | |
| while(!(r.endContainer==region && r.endOffset==0) && rect.bottom>pos.top+sizingH) { | |
| debug(); r.myMoveOneCharLeft(); rect = r.myGetExtensionRect(); fixNullRect(); | |
| } | |
| debug() | |
| // | |
| // note: if we optimized via hit-testing, this may be wrong | |
| // if next condition does not hold, we're fine. | |
| // otherwhise we must restart without optimization... | |
| // | |
| // if the selected content is possibly off-target | |
| var optimizationFailled = false; if(!dontOptimize) { | |
| var current = r.endContainer; | |
| while(current = cssRegionsHelpers.getAllLevelPreviousSibling(current, region)) { | |
| if(Node.getBoundingClientRect(current).bottom > pos.top + sizingH) { | |
| r.setStart(region,0); | |
| r.setEnd(region,0); | |
| optimizationFailled=true; | |
| dontOptimize=true; | |
| break; | |
| } | |
| } | |
| } | |
| } while(optimizationFailled) | |
| // | |
| // note: we should not break the content inside monolithic content | |
| // if we do, we need to change the selection to avoid that | |
| // | |
| // move the selection before the monolithic ancestors | |
| var current = r.endContainer; | |
| while(current !== region) { | |
| if(cssBreak.isMonolithic(current)) { | |
| r.setEndBefore(current); | |
| } | |
| current = current.parentNode; | |
| } | |
| // if the selection is not in the region anymore, add the whole region | |
| if(!r || (region !== r.endContainer && !Node.contains(region,r.endContainer))) { | |
| cssConsole.dir(r.cloneRange()); debugger; | |
| r.setStart(region,region.childNodes.length); | |
| r.setEnd(region,region.childNodes.length); | |
| } | |
| // | |
| // note: we don't want to break inside a line. | |
| // backtrack to end of previous line... | |
| // | |
| var first = r.startContainer.childNodes[r.startOffset], current = first; | |
| if(cssBreak.hasAnyInlineFlow(r.startContainer)) { | |
| while((current) && (current = current.previousSibling)) { | |
| if(cssBreak.areInSameSingleLine(current,first)) { | |
| // optimization: first and current are on the same line | |
| // so if next and current are not the same line, it will still be | |
| // the same line the "first" element is in | |
| first = current; | |
| if(current instanceof Element) { | |
| // we don't want to break inside text lines | |
| r.setEndBefore(current); | |
| } else { | |
| // get last line via client rects | |
| var lines = Node.getClientRects(current); | |
| // if the text node did wrap into multiple lines | |
| if(lines.length>1) { | |
| // move back from the end until we get into previous line | |
| var previousLineBottom = lines[lines.length-2].bottom; | |
| r.setEnd(current, current.nodeValue.length); | |
| while(rect.bottom>previousLineBottom) { | |
| r.myMoveOneCharLeft(); rect = r.myGetExtensionRect(); fixNullRect(); | |
| } | |
| // make sure we didn't exit the text node by mistake | |
| if(r.endContainer!==current) { | |
| // if we did, there's something wrong about the text node | |
| // but we can consider the text node as an element instead | |
| r.setEndBefore(current); // debugger; | |
| } | |
| } else { | |
| // we can consider the text node as an element | |
| r.setEndBefore(current); | |
| } | |
| } | |
| } else { | |
| // if the two elements are not on the same line, | |
| // then we just found a line break! | |
| break; | |
| } | |
| } | |
| } | |
| // if the selection is not in the region anymore, add the whole region | |
| if(!r || (region !== r.endContainer && !Node.contains(region,r.endContainer))) { | |
| cssConsole.dir(r.cloneRange()); debugger; | |
| r.setStart(region,region.childNodes.length); | |
| r.setEnd(region,region.childNodes.length); | |
| } | |
| // | |
| // note: the css-break spec says that a region should not be emtpy | |
| // | |
| // if we end up with nothing being selected, add the first block anyway | |
| if(r.endContainer===region && r.endOffset===0 && r.endOffset!==region.childNodes.length) { | |
| // find the first allowed break point | |
| do { | |
| //cssConsole.dir(r.cloneRange()); | |
| // move the position char-by-char | |
| r.myMoveTowardRight(); | |
| // but skip long islands of monolithic elements | |
| // since we know we cannot break inside them anyway | |
| var current = r.endContainer; | |
| while(current && current !== region) { | |
| if(cssBreak.isMonolithic(current)) { | |
| r.setStartAfter(current); | |
| r.setEndAfter(current); | |
| } | |
| current = current.parentNode; | |
| } | |
| } | |
| // do that until we reach a possible break point, or the end of the element | |
| while(!cssBreak.isPossibleBreakPoint(r,region) && !(r.endContainer===region && r.endOffset===region.childNodes.length)) | |
| } | |
| // if the selection is not in the region anymore, add the whole region | |
| if(!r || region !== r.endContainer && !Node.contains(region,r.endContainer)) { | |
| cssConsole.dir(r.cloneRange()); debugger; | |
| r.setStart(region,region.childNodes.length); | |
| r.setEnd(region,region.childNodes.length); | |
| } | |
| // now, let's try to find a break-before/break-after element before the splitting point | |
| var current = r.endContainer; if(current.hasChildNodes()) { if(r.endOffset>0) { current=current.childNodes[r.endOffset-1] } }; | |
| var first = r.endContainer.firstChild; | |
| do { | |
| if(current.style) { | |
| if(current != first) { | |
| if(/(region|all|always)/i.test(cssCascade.getSpecifiedStyle(current,'break-before',undefined,true).toCSSString())) { | |
| r.setStartBefore(current); | |
| r.setEndBefore(current); | |
| dontOptimize=true; // no algo involved in breaking, after all | |
| } | |
| } | |
| if(current !== region) { | |
| if(/(region|all|always)/i.test(cssCascade.getSpecifiedStyle(current,'break-after',undefined,true).toCSSString())) { | |
| r.setStartAfter(current); | |
| r.setEndAfter(current); | |
| dontOptimize=true; // no algo involved in breaking, after all | |
| } | |
| } | |
| } | |
| } while(current = cssRegionsHelpers.getAllLevelPreviousSibling(current, region)); | |
| // we're almost done! now, let's collect the ancestors to make some splitting postprocessing | |
| var current = r.endContainer; var allAncestors=[]; | |
| if(current.nodeType !== current.ELEMENT_NODE) current=current.parentNode; | |
| while(current !== region) { | |
| allAncestors.push(current); | |
| current = current.parentNode; | |
| } | |
| // | |
| // note: if we're about to split after the last child of | |
| // an element which has bottom-{padding/border/margin}, | |
| // we need to figure how how much of that p/b/m we can | |
| // actually keep in the first fragment | |
| // | |
| // TODO: avoid top & bottom p/b/m cuttings to use the | |
| // same variables names, it's ugly | |
| // | |
| // split bottom-{margin/border/padding} correctly | |
| if(r.endOffset == r.endContainer.childNodes.length && r.endContainer !== region) { | |
| // compute how much of the bottom border can actually fit | |
| var box = r.endContainer.getBoundingClientRect(); | |
| var excessHeight = box.bottom - (pos.top + sizingH); | |
| var endContainerStyle = getComputedStyle(r.endContainer); | |
| var availBorderHeight = parseFloat(endContainerStyle.borderBottomWidth); | |
| var availPaddingHeight = parseFloat(endContainerStyle.paddingBottom); | |
| // start by cutting into the border | |
| var borderCut = excessHeight; | |
| if(excessHeight > availBorderHeight) { | |
| borderCut = availBorderHeight; | |
| excessHeight -= borderCut; | |
| // continue by cutting into the padding | |
| var paddingCut = excessHeight; | |
| if(paddingCut > availPaddingHeight) { | |
| paddingCut = availPaddingHeight; | |
| excessHeight -= paddingCut; | |
| } else { | |
| excessHeight = 0; | |
| } | |
| } else { | |
| excessHeight = 0; | |
| } | |
| // we don't cut borders with radiuses | |
| // TODO: accept to cut the content not affected by the radius | |
| if(typeof(borderCut)==="number" && borderCut!==0) { | |
| // check the presence of a radius: | |
| var hasBottomRadius = ( | |
| parseInt(endContainerStyle.borderBottomLeftRadius)>0 | |
| || parseInt(endContainerStyle.borderBottomRightRadius)>0 | |
| ); | |
| if(hasBottomRadius) { | |
| // break before the whole border: | |
| borderCut = availBorderHeight; | |
| } | |
| } | |
| } | |
| // split top-{margin/border/padding} correctly | |
| if(r.endOffset == 0 && r.endContainer !== region) { | |
| // note: the only possibility here is that we | |
| // did split after a padding or a border. | |
| // | |
| // it can only happen if the border/padding is | |
| // too big to fit the region but is actually | |
| // the first break we could find! | |
| // compute how much of the top border can actually fit | |
| var box = r.endContainer.getBoundingClientRect(); | |
| var availHeight = (pos.top + sizingH) - pos.top; | |
| var endContainerStyle = getComputedStyle(r.endContainer); | |
| var availBorderHeight = parseFloat(endContainerStyle.borderTopWidth); | |
| var availPaddingHeight = parseFloat(endContainerStyle.paddingTop); | |
| var excessHeight = availBorderHeight + availPaddingHeight - availHeight; | |
| if(excessHeight > 0) { | |
| // start by cutting into the padding | |
| var topPaddingCut = excessHeight; | |
| if(excessHeight > availPaddingHeight) { | |
| topPaddingCut = availPaddingHeight; | |
| excessHeight -= topPaddingCut; | |
| // continue by cutting into the border | |
| var topBorderCut = excessHeight; | |
| if(topBorderCut > availBorderHeight) { | |
| topBorderCut = availBorderHeight; | |
| excessHeight -= topBorderCut; | |
| } else { | |
| excessHeight = 0; | |
| } | |
| } else { | |
| excessHeight = 0; | |
| } | |
| } | |
| } | |
| // remove bottom-{pbm} from all ancestors involved in the cut | |
| for(var i=allAncestors.length-1; i>=0; i--) { | |
| allAncestors[i].setAttribute('data-css-continued-fragment',true); | |
| if(getComputedStyle(allAncestors[i]).display.indexOf('block')>=0) { | |
| allAncestors[i].setAttribute('data-css-continued-block-fragment',true); | |
| } | |
| } | |
| if(typeof(borderCut)==="number") { | |
| allAncestors[0].removeAttribute('data-css-continued-fragment'); | |
| allAncestors[0].setAttribute('data-css-special-continued-fragment',true); | |
| allAncestors[0].style.borderBottomWidth = (availBorderHeight-borderCut)+'px'; | |
| } | |
| if(typeof(paddingCut)==="number") { | |
| allAncestors[0].removeAttribute('data-css-continued-fragment'); | |
| allAncestors[0].setAttribute('data-css-special-continued-fragment',true); | |
| allAncestors[0].style.paddingBottom = (availPaddingHeight-paddingCut)+'px'; | |
| } | |
| if(typeof(topBorderCut)==="number") { | |
| allAncestors[0].removeAttribute('data-css-continued-fragment'); | |
| allAncestors[0].setAttribute('data-css-continued-fragment',true); | |
| allAncestors[0].style.borderTopWidth = (availBorderHeight-topBorderCut)+'px'; | |
| } | |
| if(typeof(topPaddingCut)==="number") { | |
| allAncestors[0].removeAttribute('data-css-continued-fragment'); | |
| allAncestors[0].setAttribute('data-css-special-continued-fragment',true); | |
| allAncestors[0].style.paddingTop = (availPaddingHeight-topPaddingCut)+'px'; | |
| } | |
| // | |
| // note: at this point we have a collapsed range | |
| // located at the split point | |
| // | |
| // select the overflowing content | |
| r.setEnd(region, region.childNodes.length); | |
| // extract it from the current region | |
| var overflowingContent = r.extractContents(); | |
| // remove trailing whitespace from the cut element | |
| var tmp = allAncestors[0]; | |
| if(tmp && (tmp=tmp.lastChild) && !tmp.tagName && tmp.nodeValue) { | |
| var nodeValue = tmp.nodeValue.replace(/(\s|\r|\n)*$/,''); | |
| if(nodeValue) { | |
| tmp.nodeValue = nodeValue; | |
| } else { | |
| tmp.parentNode.removeChild(tmp); | |
| } | |
| } | |
| // | |
| // note: now we have to cancel out the artifacts of | |
| // the fragments cloning algorithm... | |
| // | |
| // do not forget to remove any top p/b/m on cut elements | |
| var newFragments = overflowingContent.querySelectorAll("[data-css-continued-fragment]"); | |
| for(var i=newFragments.length; i--;) { // TODO: optimize by using while loop and a simple matchesSelector. | |
| newFragments[i].removeAttribute('data-css-continued-fragment') | |
| newFragments[i].setAttribute('data-css-starting-fragment',true); | |
| } | |
| // deduct any already-used bottom p/b/m | |
| var specialNewFragment = overflowingContent.querySelector('[data-css-special-continued-fragment]'); | |
| if(specialNewFragment) { | |
| specialNewFragment.removeAttribute('data-css-special-continued-fragment') | |
| specialNewFragment.setAttribute('data-css-starting-fragment',true); | |
| if(typeof(borderCut)==="number") { | |
| specialNewFragment.style.borderBottomWidth = (borderCut)+'px'; | |
| } | |
| if(typeof(paddingCut)==="number") { | |
| specialNewFragment.style.paddingBottom = (paddingCut); | |
| } else { | |
| specialNewFragment.style.paddingBottom = '0px'; | |
| } | |
| if(typeof(topBorderCut)==="number") { | |
| specialNewFragment.removeAttribute('data-css-starting-fragment') | |
| specialNewFragment.setAttribute('data-css-special-starting-fragment',true); | |
| specialNewFragment.style.borderTopWidth = (topBorderCut)+'px'; | |
| } | |
| if(typeof(topPaddingCut)==="number") { | |
| specialNewFragment.removeAttribute('data-css-starting-fragment') | |
| specialNewFragment.setAttribute('data-css-special-starting-fragment',true); | |
| specialNewFragment.style.paddingTop = (topPaddingCut)+'px'; | |
| specialNewFragment.style.paddingBottom = '0px'; | |
| specialNewFragment.style.borderBottomWidth = '0px'; | |
| } | |
| } else if(typeof(borderCut)==="number") { | |
| // hum... there's an element missing here... {never happens anymore} | |
| try { throw new Error() } | |
| catch(ex) { setImmediate(function() { throw ex; }) } | |
| } else if(typeof(topPaddingCut)==="number") { | |
| // hum... there's an element missing here... {never happens anymore} | |
| try { throw new Error() } | |
| catch(ex) { setImmediate(function() { throw ex; }) } | |
| } | |
| // make sure empty nodes are reintroduced | |
| cssRegionsHelpers.unembedTrailingWhiteSpaceNodes(region); | |
| cssRegionsHelpers.unembedTrailingWhiteSpaceNodes(overflowingContent); | |
| // we're ready to return our result! | |
| return overflowingContent; | |
| }, | |
| enablePolyfill: function enablePolyfill() { | |
| // | |
| // [0] insert necessary css | |
| // | |
| var s = document.createElement('style'); | |
| s.setAttribute("data-css-no-polyfill", true); | |
| s.textContent = CSS_STYLE; | |
| var head = document.head || document.getElementsByTagName('head')[0]; | |
| head.appendChild(s); | |
| // | |
| // [1] when any update happens: | |
| // construct new content and region flow pairs | |
| // restart the region layout algorithm for the modified pairs | |
| // | |
| cssCascade.startMonitoringProperties( | |
| ["flow-into","flow-from","region-fragment"], | |
| { | |
| onupdate: function onupdate(element, rule) { | |
| // let's just ignore fragments | |
| if(element.getAttributeNode('data-css-regions-fragment-of')) return; | |
| // log some message in the console for debug | |
| cssConsole.dir({message:"onupdate",element:element,selector:rule.selector.toCSSString(),rule:rule}); | |
| var temp = null; | |
| // | |
| // compute the value of region properties | |
| // | |
| var flowInto = ( | |
| cssCascade.getSpecifiedStyle(element, "flow-into") | |
| .filter(function(t) { return t instanceof cssSyntax.IdentifierToken }) | |
| ); | |
| var flowIntoName = flowInto[0] ? flowInto[0].toCSSString().toLowerCase() : ""; | |
| if(flowIntoName=="none"||flowIntoName=="initial"||flowIntoName=="inherit"||flowIntoName=="default") {flowIntoName=""} | |
| var flowIntoType = flowInto[1] ? flowInto[1].toCSSString().toLowerCase() : ""; | |
| if(flowIntoType!="content") {flowIntoType="element"} | |
| var flowInto = flowIntoName ? flowIntoName + " " + flowIntoType : ""; | |
| var flowFrom = ( | |
| cssCascade.getSpecifiedStyle(element, "flow-from") | |
| .filter(function(t) { return t instanceof cssSyntax.IdentifierToken }) | |
| ); | |
| var flowFromName = flowFrom[0] ? flowFrom[0].toCSSString().toLowerCase() : ""; | |
| if(flowFromName=="none"||flowFromName=="initial"||flowFromName=="inherit"||flowFromName=="default") {flowFromName=""} | |
| var flowFrom = flowFromName; | |
| // | |
| // if the value of any property did change... | |
| // | |
| if(element.cssRegionsLastFlowInto != flowInto || element.cssRegionsLastFlowFrom != flowFrom) { | |
| // remove the element from previous regions | |
| var regionOverset = element.regionOverset; | |
| var lastFlowFrom = (cssRegions.flows[element.cssRegionsLastFlowFromName]); | |
| var lastFlowInto = (cssRegions.flows[element.cssRegionsLastFlowIntoName]); | |
| lastFlowFrom && lastFlowFrom.removeFromRegions(element); | |
| lastFlowInto && lastFlowInto.removeFromContent(element); | |
| // relayout those regions | |
| // (it's async so it will wait for us | |
| // to add the element back if needed) | |
| lastFlowFrom && regionOverset!='empty' && lastFlowFrom.relayout(); | |
| lastFlowInto && lastFlowInto.relayout(); | |
| // save some property values for later | |
| element.cssRegionsLastFlowInto = flowInto; | |
| element.cssRegionsLastFlowFrom = flowFrom; | |
| element.cssRegionsLastFlowIntoName = flowIntoName; | |
| element.cssRegionsLastFlowFromName = flowFromName; | |
| element.cssRegionsLastFlowIntoType = flowIntoType; | |
| // add the element to new regions | |
| // and relayout those regions, if deemed necessary | |
| if(flowFromName) { | |
| var lastFlowFrom = (cssRegions.flows[flowFromName] = cssRegions.flows[flowFromName] || new cssRegions.Flow(flowFromName)); | |
| lastFlowFrom && lastFlowFrom.addToRegions(element); | |
| lastFlowFrom && lastFlowFrom.relayout(); | |
| } | |
| if(flowIntoName) { | |
| var lastFlowInto = (cssRegions.flows[flowIntoName] = cssRegions.flows[flowIntoName] || new cssRegions.Flow(flowIntoName)); | |
| lastFlowInto && lastFlowInto.addToContent(element); | |
| lastFlowInto && lastFlowInto.relayout(); | |
| } | |
| } | |
| } | |
| } | |
| ); | |
| cssCascade.startMonitoringProperties( | |
| ["break-before","break-after"], | |
| {onupdate:function(element){ | |
| // avoid fragments triggering update loops | |
| if(element.getAttribute('data-css-regions-fragment-of')){return;} | |
| // update parent regions | |
| while(element) { | |
| if(element.cssRegionsLastFlowIntoName) { | |
| cssRegions.flows[element.cssRegionsLastFlowIntoName].relayout(); | |
| return; | |
| } | |
| element=element.parentNode; | |
| } | |
| }} | |
| ); | |
| // | |
| // [2] perform the OM exports | |
| // | |
| cssRegions.enablePolyfillObjectModel(); | |
| // | |
| // [3] make sure to update the region layout when all images loaded | |
| // | |
| window.addEventListener("load", | |
| function() { | |
| var flows = document.getNamedFlows(); | |
| for(var i=0; i<flows.length; i++) { | |
| flows[i].relayout(); | |
| } | |
| } | |
| ); | |
| // | |
| // [4] make sure we react to window resizes | |
| // | |
| // | |
| var lastWindowResize = 0; | |
| var relayoutModifiedFlows = function() { | |
| // specify the function did run | |
| relayoutModifiedFlows.timeout = 0; | |
| // rerun the layout | |
| var flows = document.getNamedFlows(); | |
| for(var i=0; i<flows.length; i++) { | |
| if(flows[i].lastRelayout > lastWindowResize) continue; | |
| if(flows[i].relayoutInProgress) { | |
| flows[i].relayout(); | |
| } else { | |
| flows[i].relayoutIfSizeChanged(); | |
| } | |
| } | |
| } | |
| var hasOngoingLayouts = function() { | |
| var flows = document.getNamedFlows(); | |
| for(var i=0; i<flows.length; i++) { | |
| if(flows[i].lastRelayout > lastWindowResize) continue; | |
| if(flows[i].relayoutInProgress) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| var restartOngoingLayouts = function() { | |
| var flows = document.getNamedFlows(); | |
| for(var i=0; i<flows.length; i++) { | |
| if(flows[i].lastRelayout > lastWindowResize) continue; | |
| if(flows[i].relayoutInProgress) { | |
| flows[i].relayout(); | |
| } | |
| } | |
| } | |
| window.addEventListener("resize", | |
| function() { | |
| // update the last layout flag | |
| lastWindowResize = +new Date(); | |
| // if we aren't planning a resfresh already | |
| if(!relayoutModifiedFlows.timeout) { | |
| // if we are already busy | |
| if(hasOngoingLayouts()) { | |
| // restart all layouts now | |
| setTimeout(restartOngoingLayouts, 16); | |
| // wait half a second before restarting them from now | |
| relayoutModifiedFlows.timeout = setTimeout(relayoutModifiedFlows, 500); | |
| } else { | |
| // debounce by running the resize code every 200ms | |
| relayoutModifiedFlows.timeout = setTimeout(relayoutModifiedFlows, 200); | |
| } | |
| } | |
| } | |
| ); | |
| }, | |
| // this dictionary is supposed to contains all the currently existing flows | |
| flows: Object.create ? Object.create(null) : {} | |
| }; | |
| enableObjectModel(window, document, cssRegions); | |
| return cssRegions; | |
| })(window, document); | |
| require.define('src/css-regions/polyfill.js'); | |
| //////////////////////////////////////// | |
| //require('core:polyfill-dom-matchMedia'); | |
| //require('core:polyfill-dom-classList'); | |
| //require('css-grid:polyfill'); | |
| require('src/css-regions/polyfill.js'); | |
| require.define('src/requirements.js'); | |
| window.cssPolyfills = { require: require }; | |
| })(); | |
| //# sourceMappingURL=css-regions-polyfill.js.map |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment