UNPKG

foam-framework

Version:
850 lines (751 loc) 22.1 kB
/** * @license * Copyright 2015 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ CLASS({ package: 'foam.u2', name: 'Element', requires: [ 'foam.u2.ElementValue' ], imports: [ 'dynamic', 'framed' ], onLoad: function() { console.log('Running Element.static().'); Function.prototype.toE = function(X) { var dyn = E('span'); var last = null; X.dynamic(this, function(e) { e = E('span').add(e); if ( last ) dyn.removeChild(last); //last.remove(); dyn.add(last = e); }); return dyn; } }, constants: { INITIAL: { output: function(out) { this.output_(out); this.state = this.OUTPUT; return out; }, load: function() { console.error('Must output before loading.'); }, unload: function() { console.error('Must output and load before unloading.'); }, destroy: function() { }, onSetCls: function() { }, onAddListener: function() { }, onSetStyle: function() { }, onSetAttr: function() { }, onAddChildren: function() { }, onInsertChildren: function() { }, toString: function() { return 'INITIAL'; } }, OUTPUT: { output: function(out) { // Only warn because it could be useful for debugging. console.warn('Duplicate output.'); return this.INITIAL.output.call(this, out); }, load: function() { this.state = this.LOADED; for ( var i = 0 ; i < this.elListeners.length ; i++ ) { var l = this.elListeners[i]; this.id$el.addEventListener(l[0], l[1]); } this.visitChildren('load'); }, unload: function() { this.state = this.UNLOADED; this.visitChildren('unload'); }, destroy: function() { }, onSetCls: function(cls, enabled) { throw "Mutations not allowed in OUTPUT state."; }, onAddListener: function(topic, listener) { throw "Mutations not allowed in OUTPUT state."; }, onSetStyle: function(key, value) { throw "Mutations not allowed in OUTPUT state."; }, onSetAttr: function(key, value) { throw "Mutations not allowed in OUTPUT state."; }, onAddChildren: function(c) { throw "Mutations not allowed in OUTPUT state."; }, onInsertChildren: function() { throw "Mutations not allowed in OUTPUT state."; }, toString: function() { return 'OUTPUT'; } }, LOADED: { output: function(out) { console.warn('Duplicate output.'); }, load: function() { console.error('Duplicate load.'); }, unload: function() { var e = this.id$el; if ( e ) { e.remove(); } this.state = this.UNLOADED; this.visitChildren('unload'); }, destroy: function() { }, onSetCls: function(cls, enabled) { var e = this.id$el; if ( ! e ) { console.warn('Missing Element: ', this.id); return } e.classList[enabled ? 'add' : 'remove'](cls); }, onAddListener: function(topic, listener) { this.id$el.addEventListener(topic, listener); }, onSetStyle: function(key, value) { this.id$el.style[key] = value; }, onSetAttr: function(key, value) { this.id$el[key] = value; }, onAddChildren: function() { var e = this.id$el; if ( ! e ) { console.warn('Missing Element: ', this.id); return } var out = this.createOutputStream(); for ( var i = 0 ; i < arguments.length ; i++ ) { out(arguments[i]); } e.insertAdjacentHTML('beforeend', out); for ( var i = 0 ; i < arguments.length ; i++ ) { arguments[i].load && arguments[i].load(); } }, onInsertChildren: function(children, reference, where) { var e = this.id$el; if ( ! e ) { console.warn("Missing Element: ", this.id); return; } var out = this.createOutputStream(); for ( var i = 0 ; i < children.length ; i++ ) { out(children[i]); } reference.id$el.insertAdjacentHTML(where, out); for ( var i = 0 ; i < children.length ; i++ ) { children[i].load && children[i].load(); } }, toString: function() { return 'LOADED'; } }, UNLOADED: { output: function() { }, load: function() { this.state = this.LOADED; this.visitChildren('load'); }, unload: function() { }, destroy: function() { }, onSetCls: function() { }, onAddListener: function() { }, onSetStyle: function() { }, onSetAttr: function() { }, onAddChildren: function() { }, onInsertChildren: function() { }, toString: function() { return 'UNLOADED'; } }, DESTROYED: { output: function() { throw 'Attempt to output() destroyed Element.'; }, load: function() { throw 'Attempt to load() destroyed Element.'; }, unload: function() { throw 'Attempt to unload() destroyed Element.';}, destroy: function() { }, onSetCls: function() { }, onAddListener: function() { }, onSetStyle: function() { }, onSetAttr: function() { }, onAddChildren: function() { }, onInsertChildren: function() { }, toString: function() { return 'DESTROYED'; } }, // ???: Should we disallow these? OPTIONAL_CLOSE_TAGS: { HTML: true, HEAD: true, BODY: true, P: true, DT: true, DD: true, LI: true, OPTION: true, THEAD: true, TH: true, TBODY: true, TR: true, TD: true, TFOOT: true, COLGROUP: true }, ILLEGAL_CLOSE_TAGS: { img: true, input: true, br: true, hr: true, frame: true, area: true, base: true, basefont: true, col: true, isindex: true, link: true, meta: true, param: true } }, properties: [ { model_: 'foam.u2.EIDProperty', name: 'id' }, { name: 'state', factory: function () { return this.INITIAL; } }, { name: 'nodeName', adapt: function(_, v) { // Convert to uppercase so that checks against OPTIONAL_CLOSE_TAGS // and ILLEGAL_CLOSE_TAGS work. return v.toUpperCase(); }, defaultValue: 'div' }, { name: 'attributeMap', transient: true, factory: function() { return {}; } }, { name: 'attributes', factory: function() { return []; }, postSet: function(_, attrs) { this.attributeMap = {}; for ( var i = 0 ; i < attrs.length ; i++ ) this.attributeMap[attrs[i].name] = attrs[i]; } }, { name: 'classes', factory: function() { return {}; } }, { name: 'css', factory: function() { return {}; } }, { name: 'childNodes', factory: function() { return []; } }, { name: 'elListeners', factory: function() { return []; } }, { name: 'children', transient: true, getter: function() { return this.childNodes.filter(function(c) { return typeof c !== 'string'; }); } }, { name: 'outerHTML', transient: true, getter: function() { return this.output(this.createOutputStream()).toString(); } }, { name: 'innerHTML', transient: true, getter: function() { return this.outputInnerHTML(this.createOutputStream()).toString(); } } ], templates: [ function CSS() {/* .foam-u2-Element-hidden { display: none !important; } */} ], methods: [ function init() { this.SUPER(); for ( var i = 0 ; i < this.model_.templates.length ; i++ ) { var t = this.model_.templates[i]; if ( t.name === 'CSS' ) { t.futureTemplate(function() { X.addStyle(this); }.bind(this)); break; } } return this.initE(); }, function initE() {}, function E(opt_nodeName) { var e = this.X.elementForName(opt_nodeName); if ( ! e ) { e = foam.u2.Element.create(null, this.Y); if ( opt_nodeName ) e.nodeName = opt_nodeName; } return e; }, function attrValue(opt_name, opt_event) { var args = { element: this }; if ( opt_name ) args.property = opt_name; if ( opt_event ) args.event = opt_event; return this.ElementValue.create(args); }, // // State // function onSetAttr(key, value) { this.state.onSetAttr.call(this, key, value); }, function onAddChildren(/* vargs */) { this.state.onAddChildren.apply(this, arguments); }, function onAddListener(topic, listener) { this.state.onAddListener.call(this, topic, listener); }, function onSetStyle(key, value) { this.state.onSetStyle.call(this, key, value); }, function onSetCls(cls, add) { this.state.onSetCls.call(this, cls, add); }, function visitChildren(methodName) { for (var i = 0; i < this.childNodes.length; i++) { var c = this.childNodes[i]; c[methodName] && c[methodName].call(c); } }, // // Focus // function focus() { }, function blur() { }, // // Visibility // function show(opt_shown) { if ( opt_shown ) { this.removeCls('foam-u2-Element-hidden'); } else { this.cls('foam-u2-Element-hidden'); } return this; }, function hide(opt_hidden) { return this.show(! (opt_hidden || true)); }, // // Lifecycle // function load() { this.state.load.call(this); }, function unload() { this.state.unload.call(this); }, function destroy() { this.state.destroy.call(this); }, // // DOM Compatibility // function setAttribute(name, value) { var attr = this.getAttributeNode(name); if ( attr ) { attr.value = value; } else { attr = {name: name, value: value}; this.attributes.push(attr); this.attributeMap[name] = attr; } this.onSetAttr(name, value); }, function removeAttribute(name) { // TODO }, function getAttributeNode(name) { return this.attributeMap[name]; }, function getAttribute(name) { var attr = this.getAttributeNode(name); return attr && attr.value; }, function appendChild(c) { this.childNodes.push(c); }, function removeChild(c) { for ( var i = 0 ; i < this.childNodes.length ; ++i ) { if ( this.childNodes[i] === c ) { this.childNodes.splice(i, 1); c.remove(); break; } } }, function remove() { this.unload(); }, // // DOM-like // function removeCls(cls) { if ( cls ) { delete this.classes[cls]; this.onSetCls(cls, false); } }, // // Fluent Methods // function setID(id) { this.id = id; return this; }, function on(topic, listener) { this.elListeners.push([topic, listener]); this.onAddListener(topic, listener); return this; }, function cls(cls, opt_enabled, opt_negate) { function negate(a, b) { return b ? a : ! a; } if ( typeof opt_enabled === 'function' ) { var fn = opt_enabled; this.dynamic(fn, function(value) { this.cls(cls, value, opt_negate); }.bind(this)); } else if ( Value.isInstance(opt_enabled) ) { var value = opt_enabled; var l = function() { this.cls(cls, value.get(), opt_negate); }.bind(this); value.addListener(l); l(); } else { var enabled = negate(opt_enabled === undefined ? true : opt_enabled, opt_negate); this.classes[cls] = enabled; this.onSetCls(cls, enabled); } return this; }, function cls2(cls) { if ( typeof cls === 'function' ) { var lastValue = null; this.dynamic(cls, function(value) { this.cls2_(lastValue, value); lastValue = value; }.bind(this)); } else if ( Value.isInstance(cls) ) { var lastValue = null; var l = function() { var v = cls.get(); this.cls2_(lastValue, v); lastValue = v; }.bind(this); cls.addListener(l); l(); } else { this.cls2_(null, cls); } return this; }, function cls2_(oldClass, newClass) { if ( oldClass === newClass ) return; this.removeCls(oldClass); if ( newClass ) { this.classes[newClass] = true; this.onSetCls(newClass, true); } }, function dynamicAttr_(key, fn) { var self = this; this.dynamic(fn, function(value) { self.setAttribute(key, value); }); }, function valueAttr_(key, value) { var l = function() { this.setAttribute(key, value.get()); }.bind(this); value.addListener(l); l(); }, function attrs(map) { for ( var key in map ) { var value = map[key]; if ( typeof value === 'function' ) this.dynamicAttr_(key, value); else if ( Value.isInstance(value) ) this.valueAttr_(key, value); else this.setAttribute(key, value); } return this; }, function dynamicStyle_(key, fn) { this.dynamic(fn, function(value) { this.style_(key, value); }.bind(this)); }, function valueStyle_(key, v) { var l = function(value) { this.style_(key, v.get()); }.bind(this); v.addListener(l); l(); }, function style_(key, value) { this.css[key] = value; this.onSetStyle(key, value); return this; }, function style(map) { for ( var key in map ) { var value = map[key]; if ( typeof value === 'function' ) this.dynamicStyle_(key, value); else if ( Value.isInstance(value) ) this.valueStyle_(key, value); else this.style_(key, value); } return this; }, function valueE_(value) { var dyn = E('span'); var last = null; var l = function() { var e = E('span'); /*if ( value.get() ) */e.add(value.get() || ''); if ( last ) dyn.removeChild(last); //last.remove(); dyn.add(last = e); }; value.addListener(this.framed(l)); l(); return dyn; }, function tag(opt_nodeName) { var c = this.E(opt_nodeName || 'br'); this.add(c); return this; }, function start(opt_nodeName) { var c = this.E(opt_nodeName); c.parent_ = this; this.add(c); return c; }, function end() { var p = this.parent_; this.parent_ = null; return p; }, function add(/* vargs */) { for ( var i = 0 ; i < arguments.length ; i++ ) { var c = arguments[i]; // Remove null values if ( c === undefined || c === null ) { Array.prototype.splice.call(arguments, i, 1); i--; continue; } else if ( Array.isArray(c) ) { Array.prototype.splice.apply(arguments, [i, 1].concat(c)); i--; continue; } else if ( c.toE ) arguments[i] = c.toE(this.Y); else if ( Value.isInstance(c) ) arguments[i] = this.valueE_(c); } if ( arguments.length ) { this.childNodes.push.apply(this.childNodes, arguments); this.onAddChildren.apply(this, arguments); } return this; }, function insertBefore(child, reference) { return this.insertAt_(child, reference, true); }, function insertAfter(child, reference) { return this.insertAt_(child, reference, false); }, function addBefore(reference/* vargs */) { var children = []; for ( var i = 1 ; i < arguments.length ; i++ ) { children.push(arguments[i]); } return this.insertAt_(children, reference, true); }, function insertAt_(children, reference, before) { var i = this.childNodes.indexOf(reference); if ( i == -1 ) { console.warn("Reference node isn't a child of this."); return this; } var index = before ? i : (i + 1); if ( Array.isArray(children) ) { this.childNodes.splice.apply(this.childNodes, [index, 0].concat(children)); } else { this.childNodes.splice(index, 0, children); } this.state.onInsertChildren.call(this, Array.isArray(children) ? children : [children], reference, before ? 'beforebegin' : 'afterend'); return this; }, function removeAllChildren() { while ( this.childNodes.length ) this.removeChild(this.childNodes[0]); }, function setChildren(value) { /** value -- a Value of an array of children which set this elements contents, replacing old children **/ var l = function() { this.removeAllChildren(); this.add.apply(this, value.get()); }.bind(this); value.addListener(l); l(); return this; }, // // Output Methods // function output(out) { return this.state.output.call(this, out); }, function outputInnerHTML(out) { for ( var i = 0 ; i < this.childNodes.length ; i++ ) out(this.childNodes[i]/*.toString()*/); return out; }, function createOutputStream() { var self = this; var buf = []; var f = function templateOut(/* arguments */) { for ( var i = 0 ; i < arguments.length ; i++ ) { var o = arguments[i]; if ( o === null || o === undefined ) { // NOP } else if ( typeof o === 'string' ) { buf.push(o); } else if ( typeof o === 'number' ) { buf.push(o); } else if ( X.foam.u2.Element.isInstance(o) ) { o.output(f); } else { if ( o && o.toView_ ) o = o.toView_(); if ( ! ( o === null || o === undefined ) ) { if ( o.toHTML ) { buf.push(o.toHTML()); } else { buf.push(o); } // TODO(kgr): Figure out what this line was supposed to do. //if ( o.initHTML && self && obj.addChild ) self.addChild(o); } } } }; f.toString = function() { if ( buf.length === 0 ) return ''; if ( buf.length > 1 ) buf = [buf.join('')]; return buf[0]; } return f; }, function write(opt_X) { opt_X = opt_X || GLOBAL; /* For debugging, not production. */ (opt_X.document || document).writeln(this.outerHTML); this.load(); return this; }, function output_(out) { /** Output the element without transitioning to the OUTPUT state. **/ out('<', this.nodeName); if ( this.id ) out(' id="', this.id, '"'); var first = true; for ( var key in this.classes ) { if ( first ) { out(' class="'); first = false; } else { out(' '); } out(key); } if ( ! first ) out('"'); first = true; for ( var key in this.css ) { var value = this.css[key]; if ( first ) { out(' style="'); first = false; } out(key, ':', value, ';'); } if ( ! first ) out('"'); for ( var i = 0 ; i < this.attributes.length ; i++ ) { var attr = this.attributes[i]; var value = this.attributes[i].value; if ( value ) { out(' ', attr.name); if ( value !== undefined ) out('="', value, '"'); } } if ( ! this.ILLEGAL_CLOSE_TAGS[this.nodeName] && ( ! this.OPTIONAL_CLOSE_TAGS[this.nodeName] || this.childNodes.length ) ) { out('>'); this.outputInnerHTML(out); out('</', this.nodeName); } out('>'); }, function toString() { var s = this.createOutputStream(); this.output_(s); return s.toString(); }, function toHTML() { return this.outerHTML; }, function initHTML() { this.load(); }, // // Template Support (internal) // function a() { return this.add.apply(this, arguments); }, function c() { return this.cls2.apply(this, arguments); }, function e() { return this.end(); }, function g(opt_nodeName) { return this.tag(opt_nodeName); }, function i(id) { return this.setID(id); }, function o(m) { for ( var k in m ) this.on(k, m[k]); return this; }, function p(a) { a[0] = this; return this; }, function s(opt_nodeName) { return this.start(opt_nodeName); }, function t(as) { return this.attrs(as); }, function x(m) { for ( var k in m ) this.X.set(k, m[k]); return this; }, function y() { return this.style.apply(this, arguments); }, ] }); /* TODO: focus?, compile, deepClone, pass data, computedStyle, don't clone if literal */