Skip to content

Commit

Permalink
First pass at ember-metal-views
Browse files Browse the repository at this point in the history
  • Loading branch information
ebryn authored and rwjblue committed Aug 19, 2014
1 parent b7ab19f commit aa7f918
Show file tree
Hide file tree
Showing 12 changed files with 1,050 additions and 0 deletions.
1 change: 1 addition & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"debug": false,
"devel": false,
"eqeqeq": true,
"esnext": true,
"evil": true,
"forin": false,
"immed": false,
Expand Down
152 changes: 152 additions & 0 deletions packages/ember-metal-views/lib/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { addObserver } from "ember-metal/observer";
import run from "ember-metal/run_loop";
import { get } from "ember-metal/property_get";

/*
var type = Ember.typeOf(value);
// if this changes, also change the logic in ember-handlebars/lib/helpers/binding.js
if (name !== 'value' && (type === 'string' || (type === 'number' && !isNaN(value)))) {
if (value !== elem.attr(name)) {
elem.attr(name, value);
}
} else if (name === 'value' || type === 'boolean') {
if (Ember.isNone(value) || value === false) {
// `null`, `undefined` or `false` should remove attribute
elem.removeAttr(name);
elem.prop(name, '');
} else if (value !== elem.prop(name)) {
// value should always be properties
elem.prop(name, value);
}
} else if (!value) {
elem.removeAttr(name);
}
*/

function setAttribute(name, key) {
var value = get(this, key),
type = typeof value,
el = this.element;

if (!el) { return; }

if (name === 'value' || type === 'boolean') {
if (value || value === 0) {
el[name] = value;
} else {
el.removeAttribute(name);
el[name] = '';
}
} else {
if (value) { el.setAttribute(name, value); } else { el.removeAttribute(name); }
}
}

// HOOK
function setupAttribute(view, attributeKey, attributeName) {
setAttribute.call(view, attributeName, attributeKey);

addObserver(view, attributeKey, null, function() {
run.schedule('render', this, setAttribute, attributeName, attributeKey);
});
}

var STRING_DECAMELIZE_REGEXP = (/([a-z\d])([A-Z])/g);
function decamelize(str) {
return str.replace(STRING_DECAMELIZE_REGEXP, '$1_$2').toLowerCase();
}

var STRING_DASHERIZE_REGEXP = (/[ _]/g);
function dasherize(str) {
return decamelize(str).replace(STRING_DASHERIZE_REGEXP,'-');
}

function changeClass(truthyClass, falsyClass, key) {
var value = get(this, key), // FIXME: have to use get for property paths here
type = typeof value,
el = this.element;

if (!el) { return; }

if (!truthyClass && !falsyClass) {
truthyClass = dasherize(key);
}
if (type === 'string') {
if (value) {
if (truthyClass) { el.classList.add(value); }
}
} else if (value) {
if (falsyClass) { el.classList.remove(falsyClass); }
if (truthyClass) { el.classList.add(truthyClass); }
} else {
if (truthyClass) { el.classList.remove(truthyClass); }
if (falsyClass) { el.classList.add(falsyClass); }
}
}

// TODO: decouple from classList
function setupClassNameBinding(view, key, truthyClass, falsyClass) {
changeClass.call(view, truthyClass, falsyClass, key);

addObserver(view, key, null, function() {
run.schedule('render', this, changeClass, truthyClass, falsyClass, key);
});
}

function setupClassNameBindings(view) {
var classNameBindings = view.classNameBindings,
className, parts;

if (!classNameBindings || classNameBindings.length === 0) { return; }

for (var i = 0, l = classNameBindings.length; i < l; i++) {
className = classNameBindings[i]; // TODO: support className aliases
parts = _parseClassNameBindingString(className);
setupClassNameBinding(view, parts[0], parts[1], parts[2]); // FIXME: teardown
}
}

function setupAttributeBindings(view) {
var attributeBindings = view.attributeBindings,
attribute, parts;

if (!attributeBindings || attributeBindings.length === 0) { return; }

for (var i = 0, l = attributeBindings.length; i < l; i++) {
attribute = attributeBindings[i]; // TODO: support attribute aliases
parts = _parseAttributeString(attribute);
setupAttribute(view, parts[0], parts[1]); // FIXME: teardown
}
}

function setupClassNames(view) {
var classNames = view.classNames,
el = view.element;

if (typeof classNames === 'string') {
el.setAttribute('class', classNames);
} else if (classNames && classNames.length) {
if (classNames.length === 1) { // PERF: avoid join'ing unnecessarily
el.setAttribute('class', classNames[0]);
} else {
el.setAttribute('class', classNames.join(' ')); // TODO: faster way to do this?
}
}
}

function _parseAttributeString(str) {
var parts = str.split(':');
if (parts.length === 1) {
parts.push(str);
}
return parts;
}


function _parseClassNameBindingString(str) {
var parts = str.split(':');
return parts;
}

export { setupClassNames, setupClassNameBindings, setupAttributeBindings };
17 changes: 17 additions & 0 deletions packages/ember-metal-views/lib/dom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function querySelector(selector) {
return document.querySelector(selector);
}

function createElement(tagName) {
return document.createElement(tagName);
}

function addEventListener(element, name, fn, capture) {
return element.addEventListener(name, fn, capture);
}

function removeEventListener(element, name, fn) {
return element.addEventListener(name, fn);
}

export { querySelector, createElement, addEventListener, removeEventListener };
98 changes: 98 additions & 0 deletions packages/ember-metal-views/lib/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { addEventListener, removeEventListener } from "ember-metal-views/dom";

// Hash lookup by view ID for event delegation
var views = {},
_views = views,
guid = 0;

function findContainingView(el) {
var view;
while (el && !(view = views[el.id])) { // TODO: use a class and querySelector instead?
el = el.parentElement;
}
return view;
}

function tryToDispatchEvent(view, type, event) {
try {
view[type](event);
} catch(e) {

}
}

function eventHandler(event) {
var view = findContainingView(event.target);
if (view) { tryToDispatchEvent(view, events[event.type], event); }
}

var events = {
touchstart : 'touchStart',
touchmove : 'touchMove',
touchend : 'touchEnd',
touchcancel : 'touchCancel',
keydown : 'keyDown',
keyup : 'keyUp',
keypress : 'keyPress',
mousedown : 'mouseDown',
mouseup : 'mouseUp',
contextmenu : 'contextMenu',
click : 'click',
dblclick : 'doubleClick',
mousemove : 'mouseMove',
focusin : 'focusIn',
focusout : 'focusOut',
mouseenter : 'mouseEnter',
mouseleave : 'mouseLeave',
submit : 'submit',
input : 'input',
change : 'change',
dragstart : 'dragStart',
drag : 'drag',
dragenter : 'dragEnter',
dragleave : 'dragLeave',
dragover : 'dragOver',
drop : 'drop',
dragend : 'dragEnd'
};

var eventNames = Object.keys(events);

var eventDispatcherActive = false;

function setupEventDispatcher() {
if (!eventDispatcherActive) {
for (var i = 0, l = eventNames.length; i < l; i++) {
addEventListener(document, eventNames[i], eventHandler, false);
}
eventDispatcherActive = true;
}
}

function reset() {
guid = 0;
// views = {}; // FIXME: setting Ember.View.views is a hack
// FIXME: hack to keep object reference the same
for (var key in views) {
delete views[key];
}
eventDispatcherActive = false;
for (var i = 0, l = eventNames.length; i < l; i++) {
removeEventListener(document, eventNames[i], eventHandler);
}
}

function lookupView(id) {
return views[id];
}

function setupView(view) {
var elementId = view.elementId = view.elementId || guid++; // FIXME: guid should be prefixed
views[elementId] = view;
}

function teardownView(view) {
delete views[view.elementId];
}

export { lookupView, setupView, teardownView, events, setupEventDispatcher, reset, _views };

0 comments on commit aa7f918

Please sign in to comment.