src/dynamic.js
Scoped.define("module:Richeditor", [
"dynamics:Dynamic",
"base:Strings",
"browser:Dom",
"browser:Events",
"browser:Selection",
"base:Objs",
"base:Types"
], function (Dynamic, Strings, Dom, DomEvents, Selection, Objs, Types, scoped) {
var Cls = Dynamic.extend({scoped: scoped}, function (inherited) {
return {
template : "<div></div>",
constructor: function (options) {
inherited.constructor.call(this, options);
this._domEvents = this.auto_destroy(new DomEvents());
this.set("content", this.initialContent || "");
this.__wasKeyInput = false;
this.__enableContentChange = false;
this.on("select", function () {
this.trigger("element");
});
this.on("change:content", function (value) {
if (this.editor() && this.__enableContentChange)
this.editor().innerHTML = value;
}, this);
this.properties().computed("text", function () {
return Strings.strip_html(this.get("content") || "");
}, ["content"]);
this._domEvents.on(window, "selectionchange", function () {
if (this.hasFocus())
this.trigger("select");
}, this);
this.__caretElementStack = {};
this.on("select", function () {
this.__caretClearElementStack();
}, this);
this.on("keyinput", function () {
this.__caretCharacterAdded();
}, this);
},
editor: function () {
return Dom.unbox(this.activeElement());
},
_afterActivate: function () {
var e = this.editor();
e.contentEditable = true;
e.innerHTML = this.get("content");
this._domEvents.on(e, "blur", function () {
this.trigger("leave");
}, this);
this._domEvents.on(e, "focus", function () {
this.trigger("enter");
}, this);
this._domEvents.on(e, "keypress", function (e) {
this.__wasKeyInput = false;
if (e.which !== 0)
this.__wasKeyInput = true;
}, this);
this._domEvents.on(e, "input", function (e) {
this.__enableContentChange = false;
this.set("content", e.innerHTML);
this.__enableContentChange = true;
if (this.__wasKeyInput) {
this.__wasKeyInput = false;
this.trigger("keyinput");
}
}, this);
},
focus: function () {
this.editor().focus();
},
caretNodeOffset: function () {
return Selection.selectionEndOffset();
},
hasParentElement : function(element) {
return this.isSelected() ? this.selectionHasParentElement(element) : this.caretHasParentElement(element);
},
setParentElement : function(element, value) {
if (this.isSelected())
this.selectionSetParentElement(element, value);
else
this.caretSetParentElement(element, value);
},
isSelected : function() {
return Selection.selectionContained(this.editor()) && Selection.selectionNonEmpty();
},
selectionSetParentElement: function (element, value) {
if (!this.isSelected())
return;
var has = this.selectionHasParentElement(element);
if (Types.is_undefined(value))
value = !has;
if (value == has)
return;
if (value)
this.selectionAddParentElement(element);
else
this.selectionRemoveParentElement(element);
},
caretSetParentElement: function (element, value) {
var has = this.caretHasParentElement(element);
if (Types.is_undefined(value))
value = !has;
if (value == has)
return;
if (value)
this.caretAddParentElement(element);
else
this.caretRemoveParentElement(element);
},
caretAddParentElement : function(element) {
if (this.caretHasParentElement(element))
return;
this.__caretElementStack[element] = true;
this.trigger("element");
},
__caretClearElementStack: function () {
this.__caretElementStack = {};
},
caretRemoveParentElement : function(element) {
if (!this.caretHasParentElement(element))
return;
this.__caretElementStack[element] = false;
this.trigger("element");
},
contentSiblings: function (node) {
var result = [];
node.parentNode.childNodes.forEach(function (sibling) {
if (sibling != node)
result.push(sibling);
});
return result;
},
selectionAncestor: function () {
return Selection.selectionAncestor();
},
selectionLeaves: function () {
return Selection.selectionLeaves();
},
selection: function () {
return Selection.selectionNodes();
},
caretNode : function() {
return Selection.selectionStartNode();
},
selectionRemoveParentElement : function(element) {
if (!this.isSelected())
return;
Selection.selectionSplitOffsets();
var nodes = Selection.selectionNodes();
nodes.forEach(function (node) {
this.remove_tag_from_parent_path(node, element, this.editor());
}, this);
Selection.selectRange(nodes[0], nodes[nodes.length - 1]);
},
hasFocus: function () {
var current = document.activeElement;
while (current) {
if (current == this.editor())
return true;
current = current.parentElement;
}
return false;
},
__caretCharacterAdded: function () {
var yesTags = [];
var noTags = [];
Objs.iter(this.__caretElementStack, function (value, tag) { (value ? yesTags : noTags).push(tag); });
var node = Dom.splitNode(this.caretNode(), this.caretNodeOffset() - 1, this.caretNodeOffset());
var i = null;
for (i = 0; i < noTags.length; ++i)
this.remove_tag_from_parent_path(node, noTags[i], this.editor());
for (i = 0; i < yesTags.length; ++i) {
var element = document.createElement(yesTags[i]);
Dom.elementInsertBefore(element, node);
element.appendChild(node);
node = element;
}
Selection.selectNode(node, 1);
},
selectionHasParentElement : function(element) {
if (!this.isSelected())
return false;
element = Types.is_string(element) ? this.editor().querySelector(element) : element;
var current = this.selectionAncestor();
while (current) {
if (current == element)
return true;
if (current == this.editor())
break;
current = current.parentElement;
}
return Objs.all(this.selectionLeaves(), function (node) {
while (node) {
if (node == element)
return true;
if (node == this.editor())
return false;
node = node.parentElement;
}
return false;
}, this);
},
caretHasParentElement : function(element) {
if (Types.is_defined(this.__caretElementStack[element]))
return this.__caretElementStack[element];
element = Types.is_string(element) ? this.editor().querySelector(element) : element;
var current = this.caretNode();
while (current) {
if (current == element)
return true;
if (current == this.editor())
break;
current = current.parentElement;
}
return false;
},
selectionAddParentElement : function (element) {
if (!this.isSelected())
return;
element = Types.is_string(element) ? this.editor().querySelector(element) : element;
Selection.selectionSplitOffsets();
var nodes = Selection.selectionNodes();
for (var i = 0; i < nodes.length; ++i) {
var current = nodes[i];
while (current && current != element && current != this.editor())
current = current.parentElement;
if (current != element) {
var e = document.createElement(element);
Dom.elementInsertBefore(e, nodes[i]);
e.appendChild(nodes[i]);
nodes[i] = e;
}
}
Selection.selectRange(nodes[0], nodes[nodes.length - 1]);
},
remove_tag_from_parent_path: function (node, tag, context) {
tag = tag.toLowerCase();
var wrap = function (sibling) {
var element = document.createElement(tag);
Dom.elementInsertBefore(element, sibling);
element.appendChild(sibling);
};
var unwrap = function (unwrap) {
while (unwrap.childNodes.length > 0)
Dom.elementInsertBefore(unwrap.childNodes[0], unwrap);
unwrap.parentElement.removeChild(unwrap);
};
var current = node.parentElement;
while (current && current != this.editor() && current != context) {
if (current.tagName.toLowerCase() === tag) {
while (node != this.editor() && node && node != current) {
this.contentSiblings(node).forEach(wrap);
node = node.parentElement;
}
var nodes = [];
for (var i = 0; i < current.childNodes.length; ++i)
nodes.push(current.childNodes[i]);
nodes.forEach(unwrap);
}
current = current.parentElement;
}
}
};
});
Cls.register("ba-richeditor");
return Cls;
});