Skip to content

Commit

Permalink
Many improvements around <span> as the default selection and loss of …
Browse files Browse the repository at this point in the history
…focus in

 general.  We still have some small issues when the default wrap is set to <spon>
 but given that things are a bit touchy, I will wait to see if there are issues.
  • Loading branch information
JoelParke committed Aug 15, 2016
1 parent f330b8b commit c39f20d
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 40 deletions.
144 changes: 110 additions & 34 deletions src/DOM.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
angular.module('textAngular.DOM', ['textAngular.factories'])
.factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', function(taSelection, taBrowserTag, $document){
.factory('taExecCommand', ['taSelection', 'taBrowserTag', '$document', 'textAngularManager', function(taSelection, taBrowserTag, $document, textAngularManager){
var listToDefault = function(listElement, defaultWrap){
var $target, i;
// if all selected then we should remove the list
Expand Down Expand Up @@ -33,31 +33,108 @@ angular.module('textAngular.DOM', ['textAngular.factories'])
selectLi($target.find('li')[0]);
};
return function(taDefaultWrap, topNode){
// NOTE: here we are dealing with the html directly from the browser and not the html the user sees.
// IF you want to modify the html the user sees, do it when the user does a switchView
taDefaultWrap = taBrowserTag(taDefaultWrap);
return function(command, showUI, options, defaultTagAttributes){
var i, $target, html, _nodes, next, optionsTagName, selectedElement;
var i, $target, html, _nodes, next, optionsTagName, selectedElement, ourSelection;
var defaultWrapper = angular.element('<' + taDefaultWrap + '>');
try{
if (taSelection.getSelection) {
ourSelection = taSelection.getSelection();
}
selectedElement = taSelection.getSelectionElement();
console.log('selectedElement', selectedElement);
//console.log('selectedElement', selectedElement);
// special checks and fixes when we are selecting the whole container
var __h, _innerNode;
/* istanbul ignore next */
if (selectedElement.tagName.toLowerCase() === 'div' && /taTextElement.+/.test(selectedElement.id)) {
if (selectedElement.tagName.toLowerCase() === 'div' &&
/taTextElement.+/.test(selectedElement.id) &&
ourSelection && ourSelection.start &&
ourSelection.start.offset === 1 &&
ourSelection.end.offset === 1) {
// opps we are actually selecting the whole container!
var __h = selectedElement.innerHTML;
//console.log('selecting whole container!');
__h = selectedElement.innerHTML;
if (/<br>/i.test(__h)) {
// Firefox adds <br>'s and so we remove the <br>
__h = __h.replace('<br>', '&#8203;'); // no space-space
__h = __h.replace(/<br>/i, '&#8203;'); // no space-space
}
if (/<br\/>/i.test(__h)) {
// Firefox adds <br/>'s and so we remove the <br/>
__h = __h.replace(/<br\/>/i, '&#8203;'); // no space-space
}
// remove stacked up <span>'s
if (/<span>(<span>)+/i.test(__h)) {
__h = __.replace(/<span>(<span>)+/i, '<span>');
}
// remove stacked up </span>'s
if (/<\/span>(<\/span>)+/i.test(__h)) {
__h = __.replace(/<\/span>(<\/span>)+/i, '<\/span>');
}
if (/<span><\/span>/i.test(__h)) {
// if we end up with a <span></span> here we remove it...
__h = __h.replace(/<span><\/span>/i, '');
}
//console.log('inner whole container', selectedElement.childNodes);
_innerNode = '<div>' + __h + '</div>';
selectedElement.innerHTML = _innerNode;
//console.log('childNodes:', selectedElement.childNodes);
taSelection.setSelectionToElementEnd(selectedElement.childNodes[0]);
selectedElement = taSelection.getSelectionElement();
} else if (selectedElement.tagName.toLowerCase() === 'span' &&
ourSelection && ourSelection.start &&
ourSelection.start.offset === 1 &&
ourSelection.end.offset === 1) {
// just a span -- this is a problem...
//console.log('selecting span!');
__h = selectedElement.innerHTML;
if (/<br>/i.test(__h)) {
// Firefox adds <br>'s and so we remove the <br>
__h = __h.replace(/<br>/i, '&#8203;'); // no space-space
}
if (/<br\/>/i.test(__h)) {
// Firefox adds <br/>'s and so we remove the <br/>
__h = __h.replace(/<br\/>/i, '&#8203;'); // no space-space
}
// remove stacked up <span>'s
if (/<span>(<span>)+/i.test(__h)) {
__h = __.replace(/<span>(<span>)+/i, '<span>');
}
console.log('inner', __h);
console.log(selectedElement.childNodes);
var _innerNode = '<' + taDefaultWrap + '>' + __h + '</' + taDefaultWrap + '>';
// remove stacked up </span>'s
if (/<\/span>(<\/span>)+/i.test(__h)) {
__h = __.replace(/<\/span>(<\/span>)+/i, '<\/span>');
}
if (/<span><\/span>/i.test(__h)) {
// if we end up with a <span></span> here we remove it...
__h = __h.replace(/<span><\/span>/i, '');
}
//console.log('inner span', selectedElement.childNodes);
// we wrap this in a <div> because otherwise the browser get confused when we attempt to select the whole node
// and the focus is not set correctly no matter what we do
_innerNode = '<div>' + __h + '</div>';
selectedElement.innerHTML = _innerNode;
console.log(selectedElement.childNodes);
taSelection.setSelectionToElementEnd(selectedElement.childNodes[0]);
selectedElement = taSelection.getSelectionElement();
console.log(selectedElement.innerHTML);
//console.log(selectedElement.innerHTML);
} else if (selectedElement.tagName.toLowerCase() === 'p' &&
ourSelection && ourSelection.start &&
ourSelection.start.offset === 1 &&
ourSelection.end.offset === 1) {
//console.log('p special');
// we need to remove the </br> that firefox adds!
__h = selectedElement.innerHTML;
if (/<br>/i.test(__h)) {
// Firefox adds <br>'s and so we remove the <br>
__h = __h.replace(/<br>/i, '&#8203;'); // no space-space
}
selectedElement.innerHTML = __h;
}
}catch(e){}
}catch(e){
/* istanbul ignore next */
// we ignore errors from testing...
if (e.codeName !== 'INDEX_SIZE_ERR') console.error(e);
}
var $selected = angular.element(selectedElement);
if(selectedElement !== undefined){
var tagName = selectedElement.tagName.toLowerCase();
Expand Down Expand Up @@ -164,7 +241,8 @@ angular.module('textAngular.DOM', ['textAngular.factories'])
$target.remove();
$target = defaultWrapper;
}
}else if($target.parent()[0].tagName.toLowerCase() === optionsTagName && !$target.parent().hasClass('ta-bind')){
}else if($target.parent()[0].tagName.toLowerCase() === optionsTagName &&
!$target.parent().hasClass('ta-bind')){
//unwrap logic for parent
var blockElement = $target.parent();
var contents = blockElement.contents();
Expand All @@ -184,7 +262,11 @@ angular.module('textAngular.DOM', ['textAngular.factories'])
}else{
// default wrap behaviour
_nodes = taSelection.getOnlySelectedElements();
if(_nodes.length === 0) _nodes = [$target[0]];
//console.log('default wrap behavior', _nodes);
if(_nodes.length === 0) {
// no nodes at all....
_nodes = [$target[0]];
}
// find the parent block element if any of the nodes are inline or text
for(i = 0; i < _nodes.length; i++){
if(_nodes[i].nodeType === 3 || !_nodes[i].tagName.match(BLOCKELEMENTS)){
Expand All @@ -197,18 +279,9 @@ angular.module('textAngular.DOM', ['textAngular.factories'])
_nodes = _nodes.filter(function(value, index, self) {
return self.indexOf(value) === index;
});
var _h;
if(angular.element(_nodes[0]).hasClass('ta-bind')){
$target = angular.element(options);
//console.log('has ta-bind -- root!');
_h = _nodes[0].innerHTML;
/* istanbul ignore next: this is hack for Firefox where it adds <br> */
if (/<span class="rangySelectionBoundary".+><br>/i.test(_h)) {
// if it is a <span rangySelectionBoundary></span> and a <br> all by itself
// this is an artifact of Firefox and so we remove the <br>
_h = _h.replace('<br>', '&#8203;'); // no space-space
}
$target[0].innerHTML = _h;
$target[0].innerHTML = _nodes[0].innerHTML;
_nodes[0].innerHTML = $target[0].outerHTML;
}else if(optionsTagName === 'blockquote'){
// blockquotes wrap other block elements
Expand All @@ -228,20 +301,18 @@ angular.module('textAngular.DOM', ['textAngular.factories'])
// regular block elements replace other block elements
for(i = 0; i < _nodes.length; i++){
$target = angular.element(options);
_h = _nodes[i].innerHTML;
/* istanbul ignore next: this is hack for Firefox where it adds <br> */
if (/<span class="rangySelectionBoundary".+><br>/i.test(_h)) {
// if it is a <span rangySelectionBoundary></span> and a <br> all by itself
// this is an artifact of Firefox and so we remove the <br>
_h = _h.replace('<br>', '&#8203;'); // no space-space
}
$target[0].innerHTML = _h;
$target[0].innerHTML = _nodes[i].innerHTML;
_nodes[i].parentNode.insertBefore($target[0],_nodes[i]);
_nodes[i].parentNode.removeChild(_nodes[i]);
}
}
}
taSelection.setSelectionToElementEnd($target[0]);
// looses focus when we have the whole container selected and no text!
// refocus on the shown display element, this fixes a bug when using firefox
$target[0].focus();
//console.log($document[0].activeElement.childNodes);
//$document[0].activeElement.childNodes[0].focus();
return;
}else if(command.toLowerCase() === 'createlink'){
var tagBegin = '<a href="' + options + '" target="' +
Expand All @@ -264,7 +335,11 @@ angular.module('textAngular.DOM', ['textAngular.factories'])
}
try{
$document[0].execCommand(command, showUI, options);
}catch(e){}
}catch(e){
/* istanbul ignore next */
// we ignore errors from testing...
if (e.codeName !== 'INDEX_SIZE_ERR') console.error(e);
}
};
};
}]).service('taSelection', ['$document', 'taDOM',
Expand Down Expand Up @@ -359,7 +434,7 @@ function($document, taDOM){
},
setSelectionToElementEnd: function (el){
var range = rangy.createRange();

range.selectNodeContents(el);
range.collapse(false);
if(el.childNodes && el.childNodes[el.childNodes.length - 1] && el.childNodes[el.childNodes.length - 1].nodeName === 'br'){
Expand Down Expand Up @@ -441,6 +516,7 @@ function($document, taDOM){

angular.element(parent).after(secondParent);
// put cursor to end of inserted content
//console.log('setStartAfter', parent);
range.setStartAfter(parent);
range.setEndAfter(parent);

Expand Down
3 changes: 2 additions & 1 deletion src/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ var _browserDetect = {

return v > 4 ? v : undef;
}()),
webkit: /AppleWebKit\/([\d.]+)/i.test(navigator.userAgent)
webkit: /AppleWebKit\/([\d.]+)/i.test(navigator.userAgent),
isFirefox: navigator.userAgent.toLowerCase().indexOf('firefox') > -1
};

// Global to textAngular to measure performance where needed
Expand Down
7 changes: 5 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ textAngular.directive("textAngular", [
});
scope.displayElements.scrollWindow.attr({'ng-hide': 'showHtml'});
if(attrs.taDefaultWrap) {
// taDefaultWrap is only applied to the text and the not the html view
// taDefaultWrap is only applied to the text and not the html view
scope.displayElements.text.attr('ta-default-wrap', attrs.taDefaultWrap);
}

Expand Down Expand Up @@ -328,6 +328,7 @@ textAngular.directive("textAngular", [
// if rangy library is loaded return a function to reload the current selection
_savedSelection = rangy.saveSelection();
return function(){
//console.log('restore to:', _savedSelection);
if(_savedSelection) rangy.restoreSelection(_savedSelection);
};
};
Expand Down Expand Up @@ -395,8 +396,9 @@ textAngular.directive("textAngular", [
$animate.enabled(false, scope.displayElements.html);
$animate.enabled(false, scope.displayElements.text);
//Show the HTML view
var _model;
/* istanbul ignore next: ngModel exists check */
/*
var _model;
if (ngModel) {
_model = ngModel.$viewValue;
} else {
Expand All @@ -408,6 +410,7 @@ textAngular.directive("textAngular", [
// they can get out of sync and when they do, we correct that here...
scope.displayElements.html.val(_model);
}
*/
if(scope.showHtml){
//defer until the element is visible
$timeout(function(){
Expand Down
28 changes: 25 additions & 3 deletions src/taBind.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'

// defaults to the paragraph element, but we need the line-break or it doesn't allow you to type into the empty element
// non IE is '<p><br/></p>', ie is '<p></p>' as for once IE gets it correct...
var _defaultVal;
var _defaultVal, _defaultTest;

var _CTRL_KEY = 0x0001;
var _META_KEY = 0x0002;
Expand Down Expand Up @@ -146,12 +146,18 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
/* istanbul ignore next: ie specific test */
if(attrs.taDefaultWrap === ''){
_defaultVal = '';
_defaultTest = (_browserDetect.ie === undefined)? '<div><br></div>' : (_browserDetect.ie >= 11)? '<p><br></p>' : (_browserDetect.ie <= 8)? '<P>&nbsp;</P>' : '<p>&nbsp;</p>';
}else{
_defaultVal = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
'<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
(_browserDetect.ie <= 8)?
'<' + attrs.taDefaultWrap.toUpperCase() + '></' + attrs.taDefaultWrap.toUpperCase() + '>' :
'<' + attrs.taDefaultWrap + '></' + attrs.taDefaultWrap + '>';
_defaultTest = (_browserDetect.ie === undefined || _browserDetect.ie >= 11)?
'<' + attrs.taDefaultWrap + '><br></' + attrs.taDefaultWrap + '>' :
(_browserDetect.ie <= 8)?
'<' + attrs.taDefaultWrap.toUpperCase() + '>&nbsp;</' + attrs.taDefaultWrap.toUpperCase() + '>' :
'<' + attrs.taDefaultWrap + '>&nbsp;</' + attrs.taDefaultWrap + '>';
}

/* istanbul ignore else */
Expand All @@ -161,7 +167,11 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
if (_taBlankTest(value)) return value;
var domTest = angular.element("<div>" + value + "</div>");
//console.log('domTest.children().length():', domTest.children().length);
//console.log('_ensureContentWrapped', domTest.children());
//console.log(value, attrs.taDefaultWrap);
if (domTest.children().length === 0) {
// if we have a <br> and the attrs.taDefaultWrap is a <p> we need to remove the <br>
//value = value.replace(/<br>/i, '');
value = "<" + attrs.taDefaultWrap + ">" + value + "</" + attrs.taDefaultWrap + ">";
} else {
var _children = domTest[0].childNodes;
Expand Down Expand Up @@ -811,17 +821,29 @@ angular.module('textAngular.taBind', ['textAngular.factories', 'textAngular.DOM'
selection = selection.parentNode;
}

if(selection.tagName.toLowerCase() !== attrs.taDefaultWrap && selection.tagName.toLowerCase() !== 'li' && (selection.innerHTML.trim() === '' || selection.innerHTML.trim() === '<br>')){
if(selection.tagName.toLowerCase() !==
attrs.taDefaultWrap &&
selection.tagName.toLowerCase() !== 'li' &&
(selection.innerHTML.trim() === '' || selection.innerHTML.trim() === '<br>')
) {
var _new = angular.element(_defaultVal);
angular.element(selection).replaceWith(_new);
taSelection.setSelectionToElementStart(_new[0]);
}
}
}
var val = _compileHtml();
if(_defaultVal !== '' && val.trim() === ''){
if(_defaultVal !== '' && (val.trim() === '' || val.trim() === '<br>')){
_setInnerHTML(_defaultVal);
taSelection.setSelectionToElementStart(element.children()[0]);
}else if(val.substring(0, 1) !== '<' && attrs.taDefaultWrap !== ''){
/* we no longer do this, since there can be comments here and white space
var _savedSelection = rangy.saveSelection();
val = _compileHtml();
val = "<" + attrs.taDefaultWrap + ">" + val + "</" + attrs.taDefaultWrap + ">";
_setInnerHTML(val);
rangy.restoreSelection(_savedSelection);
*/
}
var triggerUndo = _lastKey !== event.keyCode && UNDO_TRIGGER_KEYS.test(event.keyCode);
if(_keyupTimeout) $timeout.cancel(_keyupTimeout);
Expand Down
6 changes: 6 additions & 0 deletions test/textAngular.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ describe('textAngular', function(){
expect(stripped).toBe('');
});
});
describe('Gets the proper DOM', function () {
it('from Html', function () {
var dom = getDomFromHtml('<div><p></p></div>');
expect(dom.innerHTML).toBe('<div><p></p></div>');
});
});
});

describe('Add classes via attributes', function(){
Expand Down

0 comments on commit c39f20d

Please sign in to comment.