Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ window.qed = {
Editor: require('./editor'),
Point: require('./point'),
Range: require('./range'),
Toolbar: require('./toolbar')
Toolbar: require('./toolbar'),

InlineDecorator: require('./inline-decorator')

// TODO: Just export all the classes so people can put them together
// however they like.
Expand Down
2 changes: 2 additions & 0 deletions src/event-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,8 @@ var EventRouter = module.exports = function EventRouter(getRootElem, registry, s
}
};

// TODO: possibly replace bubble utility functions with range iterators

/**
* Alters the given point such that:
* - if there is a contextual element to apply a directional (left/right)
Expand Down
156 changes: 156 additions & 0 deletions src/inline-decorator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
'use strict';

var util = require('./util');
var assert = util.assert;

/**
* Inline styles & attributes utility
*
* wnd - optional arg to override window for testing
*/
module.exports = function InlineDecorator(wnd) {
if (!wnd) {
wnd = window;
}

var me = this;

// TODO: Generalise the Registry class for use here.
// Decide if we want to allow extensible attribute definitions,
// or extensible element types, or both (kind of an m x n problem).
// For now, hard coding the behaviour.
// Note: possible way is to abolish any querying - inline attributes
// are sourced from the model only (can have some standard extraction
// logic for pasting of arbitrary rich text) - and rendering is
// registered with the attribute (cf annotation painting in wave editor).
var attrParsers = {
};

/**
* Returns a map of attrName -> [list of values] that apply over a given range
* in the document.
*/
me.getRangeAttributes = function(range) {
assert(range && range.anchor && range.focus);

assert(util.isEditable(range.anchor.containingElement()));

var attrs = {};
for (var k in supportedAttributes) {
attrs[k] = [];
}

// Special case - if our range is collapsed,
// then we treat the
if (range.isCollapsed()) {
accumulate(range.getStart().containingElement());
return attrs;
}

var it = range.iterateRight();

// Here, we accumulate styles over all selectable content.
// That is, styles for text, and certain element boundaries
// like inter-paragraph newlines.
while (true) {
var el = it.enterElement();
if (el) {
if (isIgnored(el)) {
// jump back out to skip entire element contents.
el = it.leaveElement();

assert(el); // otherwise end is inside an ignored widget or something??
} else if (elementHasSelectableBoundary(el)) {
accumulate(el);
}

continue;
}

var el = it.leaveElement();
if (el) {
continue;
}


var txt = it.skipText();
if (txt) {
accumulate(txt.start.containingElement());
continue;
}

assert(it.isAtEnd());

return attrs;
}


function accumulate(elem) {
var computed = hackComputedStyle(elem, wnd);

var len = supportedAttributes.length;
for (var attr in supportedAttributes) {
var val = computed[attr];
if (val && attrs[attr].indexOf(val) < 0) {
attrs[attr].push(val);
}
}
}

function hackComputedStyle(elem) {
var computed = util.computedStyle(elem, wnd);

// HACK(dan): jsdom doesn't seem to implement this properly for testing afaik
// quick hack, good enough for cases tested.
if (!computed['font-weight']) {
computed = util.shallowCopy(supportedAttributes);
if (util.isElemType(elem, 'i')) {
computed['font-style'] = 'italic';
}
if (util.isElemType(elem, 'b')) {
computed['font-weight'] = 'bold';
}
}

return computed;
}
};

// todo: links
var supportedAttributes = { // defaults (unused for now?)
'font-weight' : 'normal',
'font-style' : 'normal',
'text-decoration' : 'none',
'color' : 'inherit'
};

this.getDefaults = function() {
return util.shallowCopy(supportedAttributes);
};

/**
* Returns true if the element has a selectable boundary.
* E.g. the conceptual newline betwen paragraphs.
*
* An inline styling span does not have a selectable boundary.
*/
function elementHasSelectableBoundary(el) {
return util.isBlock(el, wnd);
}

function isIgnored(el) {
// Probably some kind of fancy nested widget.
// Treat it as a style-inert black box.
if (!util.isEditable(el)) {
return true;
}

// Ignore BRs, don't let their state mess with the logic.
if (util.isElemType(el, 'br')) {
return true;
}

return false;
}

};
76 changes: 76 additions & 0 deletions src/inline-decorator.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
var _ = require('lodash');
var util = require('./util');
var assert = util.assert;
var InlineDecorator = require('./inline-decorator');
var tutil = require('./test-util');
var Point = require('./point');
var Q = require('q');

describe('InlineDecorator', function() {
var testCases = [
['|', {}],
['|a', {}],
['a|', {}],
['[a]', {}],

['<b>|</b>', {'font-weight':['bold']}],
['<b>|a</b>', {'font-weight':['bold']}],
['<b>a|</b>', {'font-weight':['bold']}],
['<b>[a]</b>', {'font-weight':['bold']}],

['<p>a|bc<i>sup</i></p><p>hello<b>sup</b></p>', {}],
['<p>abc<i>|sup</i></p><p>hello<b>sup</b></p>', {'font-style':['italic']}],
['<p>abc<i>sup</i></p><p>hello<b>|sup</b></p>', {'font-weight':['bold']}],
['<p>ab[c<i>s]up</i></p><p>hello<b>sup</b></p>', {'font-style':['italic','normal']}],
['<p>|</p>', {}]

];

it('getRangeAttributes', tutil.promised(function() {
return tutil.rangeCases(testCases, function(elem, range, expected) {

// So decorator thinks it's in an editor
elem.contentEditable = 'true';

assert(elem.$wnd);

var idec = new InlineDecorator(elem.$wnd);

var fullExpected = expandWithDefaults(expected, idec.getDefaults());
var attrs = idec.getRangeAttributes(range);

sortArrays(attrs);
sortArrays(fullExpected);

expect(attrs).toEqual(fullExpected);
}, function wrap(testContent) {
return '<div contentEditable="true">' + testContent + '</div>';
});
}));

});

function sortArrays(mapOfArrays) {
for (var k in mapOfArrays) {
mapOfArrays[k].sort();
}
}

/**
* Expands the given attrs to encompass the full set of keys with at least
* one value for each key, given the defaults.
*/
function expandWithDefaults(attrs, defaults) {

var filled = _.assign({}, attrs);
for (var k in defaults) {
if (!filled[k]) {
filled[k] = [];
}
if (filled[k].length === 0) {
filled[k].push(defaults[k]);
}
}

return filled;
}
3 changes: 2 additions & 1 deletion src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"webpack": "1.4.9",
"jsdom": "1.0.3",
"jasmine-node": "1.14.5",
"q": "1.0.1"
"q": "1.0.1",
"lodash": "3.1.0"
}
}
Loading