locomotive-aloha-rails 0.23.2.1 → 0.23.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.md +3 -3
  2. data/lib/aloha/rails/engine.rb +1 -1
  3. data/lib/aloha/rails/version.rb +2 -2
  4. data/lib/tasks/aloha-assets.rake +1 -1
  5. data/vendor/assets/javascripts/aloha/css/aloha-sidebar.css +41 -17
  6. data/vendor/assets/javascripts/aloha/css/aloha.css +2 -2
  7. data/vendor/assets/javascripts/aloha/lib/aloha/contenthandlermanager.js +19 -18
  8. data/vendor/assets/javascripts/aloha/lib/aloha/engine.js +168 -9
  9. data/vendor/assets/javascripts/aloha/lib/aloha/ephemera.js +8 -1
  10. data/vendor/assets/javascripts/aloha/lib/aloha/markup.js +3 -2
  11. data/vendor/assets/javascripts/aloha/lib/aloha/pluginmanager.js +8 -0
  12. data/vendor/assets/javascripts/aloha/lib/aloha/sidebar.js +1 -1
  13. data/vendor/assets/javascripts/aloha/lib/aloha/state-override.js +59 -37
  14. data/vendor/assets/javascripts/aloha/lib/util/arrays.js +53 -6
  15. data/vendor/assets/javascripts/aloha/lib/util/boundary-markers.js +86 -0
  16. data/vendor/assets/javascripts/aloha/lib/util/dom2.js +178 -19
  17. data/vendor/assets/javascripts/aloha/lib/util/html.js +77 -10
  18. data/vendor/assets/javascripts/aloha/lib/util/maps.js +23 -1
  19. data/vendor/assets/javascripts/aloha/lib/util/range-context.js +429 -181
  20. data/vendor/assets/javascripts/aloha/lib/util/strings.js +9 -1
  21. data/vendor/assets/javascripts/aloha/plugins/common/align/nls/de/i18n.js +4 -1
  22. data/vendor/assets/javascripts/aloha/plugins/common/block/lib/blockmanager.js +2 -5
  23. data/vendor/assets/javascripts/aloha/plugins/common/characterpicker/css/characterpicker.css +6 -3
  24. data/vendor/assets/javascripts/aloha/plugins/common/characterpicker/lib/characterpicker-plugin.js +29 -32
  25. data/vendor/assets/javascripts/aloha/plugins/common/contenthandler/lib/blockelementcontenthandler.js +41 -70
  26. data/vendor/assets/javascripts/aloha/plugins/common/dom-to-xhtml/lib/dom-to-xhtml.js +1 -1
  27. data/vendor/assets/javascripts/aloha/plugins/common/format/lib/format-plugin.js +22 -10
  28. data/vendor/assets/javascripts/aloha/plugins/common/link/css/link.css +2 -8
  29. data/vendor/assets/javascripts/aloha/plugins/common/table/lib/table-cell.js +7 -0
  30. data/vendor/assets/javascripts/aloha/plugins/common/table/lib/table-plugin.js +149 -131
  31. data/vendor/assets/javascripts/aloha/plugins/common/table/lib/table.js +77 -47
  32. data/vendor/assets/javascripts/aloha/plugins/common/ui/lib/port-helper-attribute-field.js +4 -8
  33. data/vendor/assets/javascripts/aloha/plugins/common/ui/nls/de/i18n.js +1 -0
  34. data/vendor/assets/javascripts/aloha/plugins/extra/cite/css/cite.css +0 -10
  35. data/vendor/assets/javascripts/aloha/plugins/extra/cite/lib/cite-plugin.js +4 -4
  36. data/vendor/assets/javascripts/aloha/plugins/extra/headerids/lib/headerids-plugin.js +84 -32
  37. data/vendor/assets/javascripts/aloha/plugins/extra/numerated-headers/nls/de/i18n.js +2 -1
  38. data/vendor/assets/javascripts/aloha/plugins/extra/numerated-headers/nls/i18n.js +2 -1
  39. metadata +15 -14
@@ -50,12 +50,37 @@ define([
50
50
  'U': true
51
51
  };
52
52
 
53
- // Is this list complete? What about HTML5 semantic tags?
54
- var BLOCK_TAGNAMES = [
53
+ // NB: "block-level" is not technically defined for elements that are new in
54
+ // HTML5.
55
+ var BLOCKLEVEL_ELEMENTS = [
56
+ 'address',
57
+ 'article', // HTML5
58
+ 'aside', // HTML5
59
+ 'audio', // HTML5
55
60
  'blockquote',
61
+ 'canvas', // HTML5
62
+ 'dd',
63
+ 'div',
64
+ 'dl',
65
+ 'fieldset',
66
+ 'figcaption',
67
+ 'figure',
68
+ 'footer',
69
+ 'form',
56
70
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
71
+ 'header',
72
+ 'hgroup',
73
+ 'hr',
74
+ 'noscript',
75
+ 'ol',
76
+ 'output',
57
77
  'p',
58
- 'pre'
78
+ 'pre',
79
+ 'section', // HTML5
80
+ 'table',
81
+ 'tfoot',
82
+ 'ul',
83
+ 'video' // HTML5
59
84
  ];
60
85
 
61
86
  /**
@@ -65,17 +90,20 @@ define([
65
90
  * @type {object<string, boolean>}
66
91
  */
67
92
  var blocksTagnameMap = {};
68
- Maps.fillKeys(blocksTagnameMap, BLOCK_TAGNAMES, true);
69
- Maps.fillKeys(blocksTagnameMap, Arrays.map(BLOCK_TAGNAMES, function (str) {
70
- return str.toUpperCase();
71
- }), true);
93
+ Maps.fillKeys(blocksTagnameMap, BLOCKLEVEL_ELEMENTS, true);
94
+ Maps.fillKeys(
95
+ blocksTagnameMap,
96
+ Arrays.map(BLOCKLEVEL_ELEMENTS, function (str) {
97
+ return str.toUpperCase();
98
+ }),
99
+ true
100
+ );
72
101
 
73
102
  function isBlock(node) {
74
103
  return blocksTagnameMap[node.nodeName];
75
104
  }
76
105
 
77
106
  function isIgnorableWhitespace(node) {
78
- // TODO
79
107
  return 3 === node.nodeType && !node.length;
80
108
  }
81
109
 
@@ -111,11 +139,50 @@ define([
111
139
  return found;
112
140
  }
113
141
 
142
+ function isEditingHost(node) {
143
+ return 1 === node.nodeType && "true" === node.contentEditable;
144
+ }
145
+
146
+ /**
147
+ * Starting from the given node, and working backwards through the siblings,
148
+ * find the node that satisfies the given condition.
149
+ *
150
+ * @param {HTMLElement} node The node at which to start the search.
151
+ * @param {function(HTMLElement):boolean} condition A predicate the receives
152
+ * one of children of `node`.
153
+ *
154
+ * @return {HTMLElement} The first node that meets the given condition.
155
+ */
156
+ function findNodeRight(node, condition) {
157
+ while (node && !condition(node)) {
158
+ node = node.previousSibling;
159
+ }
160
+ return node;
161
+ }
162
+
163
+ /**
164
+ * Checks if the given editable is a valid container for paragraphs.
165
+ *
166
+ * @param {Aloha.Editable} editable The editable to be checked
167
+ *
168
+ * @return {boolean} False if the editable may not contain paragraphs
169
+ */
170
+ function allowNestedParagraph(editable) {
171
+ if (editable.obj.prop("tagName") === "SPAN" ||
172
+ editable.obj.prop("tagName") === "P") {
173
+ return false;
174
+ }
175
+ return true;
176
+ }
177
+
114
178
  return {
115
- BLOCK_TAGNAMES: BLOCK_TAGNAMES,
179
+ BLOCKLEVEL_ELEMENTS: BLOCKLEVEL_ELEMENTS,
116
180
  isBlock: isBlock,
117
181
  isIgnorableWhitespace: isIgnorableWhitespace,
118
182
  isInlineFormattable: isInlineFormattable,
119
- isProppedBlock: isProppedBlock
183
+ isProppedBlock: isProppedBlock,
184
+ isEditingHost: isEditingHost,
185
+ findNodeRight: findNodeRight,
186
+ allowNestedParagraph: allowNestedParagraph
120
187
  };
121
188
  });
@@ -88,6 +88,9 @@ define([], function () {
88
88
  return map;
89
89
  }
90
90
 
91
+ /**
92
+ * Returns an array of the map's keys.
93
+ */
91
94
  function keys(map) {
92
95
  var ks = [],
93
96
  k;
@@ -99,10 +102,29 @@ define([], function () {
99
102
  return ks;
100
103
  }
101
104
 
105
+ /**
106
+ * For each mapping, call cb(value, key, map).
107
+ *
108
+ * Emulates ECMAScript edition 5 Array.forEach.
109
+ *
110
+ * Contrary to "for (key in map)" iterates only over the
111
+ * "hasOwnProperty" properties of the map, which is usually what you
112
+ * want.
113
+ */
114
+ function forEach(map, cb) {
115
+ var key;
116
+ for (key in map) {
117
+ if (map.hasOwnProperty(key)) {
118
+ cb(map[key], key, map);
119
+ }
120
+ }
121
+ }
122
+
102
123
  return {
103
124
  isEmpty: isEmpty,
104
125
  fillTuples: fillTuples,
105
126
  fillKeys: fillKeys,
106
- keys: keys
127
+ keys: keys,
128
+ forEach: forEach
107
129
  };
108
130
  });
@@ -24,16 +24,31 @@
24
24
  * provided you include this license notice and a URL through which
25
25
  * recipients can access the Corresponding Source.
26
26
  */
27
+ /**
28
+ * TODO improve restacking and joining algorithm
29
+ * TODO what do do about insignificant whitespace when pushing down or setting a context?
30
+ * TODO check contained-in rules when when pushing down or setting a context
31
+ * TODO formatStyle: in the following case the outer "font-family: arial" span should be removed.
32
+ * Can be done similar to how findReusableAncestor() works.
33
+ * <span style="font-family: arial">
34
+ * <span style="font-family: times">one</span>
35
+ * <span style="font-family: helvetica">two<span>
36
+ * </span>
37
+ */
27
38
  define([
39
+ 'jquery',
28
40
  'util/dom2',
29
41
  'util/arrays',
30
42
  'util/trees',
43
+ 'util/strings',
31
44
  'util/functions',
32
45
  'util/html'
33
46
  ], function (
47
+ $,
34
48
  Dom,
35
49
  Arrays,
36
50
  Trees,
51
+ Strings,
37
52
  Fn,
38
53
  Html
39
54
  ) {
@@ -48,10 +63,10 @@ define([
48
63
  var fn = before;
49
64
  Dom.walk(parent.firstChild, function (child) {
50
65
  if (child !== beforeAfterChild) {
51
- return fn(child, arg);
66
+ fn(child, arg);
52
67
  } else {
53
68
  fn = after;
54
- return at(child, arg);
69
+ at(child, arg);
55
70
  }
56
71
  });
57
72
  }
@@ -93,6 +108,20 @@ define([
93
108
  }
94
109
  }
95
110
 
111
+ function makePointNodeStep(pointNode, atEnd, stepOutsideInside, stepPartial) {
112
+ // Because the start node is inside the range, the end node is
113
+ // outside, and all ancestors of start and end are partially
114
+ // inside/outside (for startEnd/endEnd positions the nodes are
115
+ // also ancestors of the position).
116
+ return function (node, arg) {
117
+ if (node === pointNode && !atEnd) {
118
+ stepOutsideInside(node, arg);
119
+ } else {
120
+ stepPartial(node, arg);
121
+ }
122
+ };
123
+ }
124
+
96
125
  /**
97
126
  * Walks the boundary of the range.
98
127
  *
@@ -118,34 +147,23 @@ define([
118
147
  var end = Dom.nodeAtOffset(ec, eo);
119
148
  var startEnd = Dom.isAtEnd(sc, so);
120
149
  var endEnd = Dom.isAtEnd(ec, eo);
121
- var uptoCacChildStart = Dom.childAndParentsUntilNode(start, cac);
122
- var uptoCacChildEnd = Dom.childAndParentsUntilNode(end, cac);
123
- var cacChildStart = uptoCacChildStart.length ? uptoCacChildStart[uptoCacChildStart.length - 1] : null;
124
- var cacChildEnd = uptoCacChildEnd.length ? uptoCacChildEnd[uptoCacChildEnd.length - 1] : null;
125
- arg = carryDown(cac, arg) || arg;
126
- // Because the start node is inside the range, the end node is
127
- // outside, and all ancestors of start and end are partially
128
- // inside/outside (for startEnd/endEnd positions the nodes are
129
- // also ancestors of the position).
130
- function stepAtStart(node, arg) {
131
- return node === start && !startEnd
132
- ? stepInside(node, arg)
133
- : stepPartial(node, arg);
134
- }
135
- function stepAtEnd(node, arg) {
136
- return node === end && !endEnd
137
- ? stepOutside(node, arg)
138
- : stepPartial(node, arg);
139
- }
140
- ascendWalkSiblings(uptoCacChildStart, startEnd, carryDown, stepOutside, stepAtStart, stepInside, arg);
141
- ascendWalkSiblings(uptoCacChildEnd, endEnd, carryDown, stepInside, stepAtEnd, stepOutside, arg);
150
+ var ascStart = Dom.childAndParentsUntilNode(start, cac);
151
+ var ascEnd = Dom.childAndParentsUntilNode(end, cac);
152
+ var stepAtStart = makePointNodeStep(start, startEnd, stepInside, stepPartial);
153
+ var stepAtEnd = makePointNodeStep(end, endEnd, stepOutside, stepPartial);
154
+ ascendWalkSiblings(ascStart, startEnd, carryDown, stepOutside, stepAtStart, stepInside, arg);
155
+ ascendWalkSiblings(ascEnd, endEnd, carryDown, stepInside, stepAtEnd, stepOutside, arg);
156
+ var cacChildStart = Arrays.last(ascStart);
157
+ var cacChildEnd = Arrays.last(ascEnd);
142
158
  if (cacChildStart && cacChildStart !== cacChildEnd) {
143
159
  var next;
144
160
  Dom.walkUntilNode(cac.firstChild, stepOutside, cacChildStart, arg);
145
- next = stepAtStart(cacChildStart, arg);
161
+ next = cacChildStart.nextSibling;
162
+ stepAtStart(cacChildStart, arg);
146
163
  Dom.walkUntilNode(next, stepInside, cacChildEnd, arg);
147
164
  if (cacChildEnd) {
148
- next = stepAtEnd(cacChildEnd, arg);
165
+ next = cacChildEnd.nextSibling;
166
+ stepAtEnd(cacChildEnd, arg);
149
167
  Dom.walk(next, stepOutside, arg);
150
168
  }
151
169
  }
@@ -217,41 +235,45 @@ define([
217
235
  * @param liveRange range's boundary points should be between nodes
218
236
  * (Dom.splitTextContainers).
219
237
  *
220
- * @param isUpperBoundary args (node). Identifies exclusive upper
221
- * boundary element, only elements below which will be modified.
238
+ * @param formatter a map with the following properties
239
+ * isUpperBoundary(node) - identifies exclusive upper
240
+ * boundary element, only elements below which will be modified.
222
241
  *
223
- * @param getOverride(node). Returns a node's override, or
224
- * null if the node does not provide an override. The topmost node
225
- * for which getOverride returns a non-null value is the topmost
226
- * override. If there is a topmost override, and it is below the
227
- * upper boundary element, it will be cleared and pushed down.
242
+ * getOverride(node) - returns a node's override, or null/undefined
243
+ * if the node does not provide an override. The topmost node for
244
+ * which getOverride returns a non-null value is the topmost
245
+ * override. If there is a topmost override, and it is below the
246
+ * upper boundary element, it will be cleared and pushed down.
247
+ * A node with an override must not also provide the context:
248
+ * !(null != getOverride(node) && hasContext(node))
228
249
  *
229
- * @param clearOverride(node). Should clear the given node of an
230
- * override. The given node may or may not have an override
231
- * set. Will be invoked shallowly for all ancestors of start and end
232
- * containers (up to isUpperBoundary or hasContext). May perform
233
- * mutations as explained above.
250
+ * clearOverride(node) - should clear the given node of an
251
+ * override. The given node may or may not have an override
252
+ * set. Will be invoked shallowly for all ancestors of start and end
253
+ * containers (up to isUpperBoundary or hasContext). May perform
254
+ * mutations as explained above.
234
255
  *
235
- * @parma clearOverrideRec(node). Like clearOverride but
236
- * should clear the override recursively.
256
+ * clearOverrideRec(node) - like clearOverride but should clear
257
+ * the override recursively. If not provided, clearOverride will
258
+ * be applied recursively.
237
259
  *
238
- * @param pushDownOverride(node, override). Applies the given
239
- * override to node. Should check whether the given node doesn't
240
- * already provide its own override, in which case the given
241
- * override should not be applied. May perform mutations as
242
- * explained above.
260
+ * pushDownOverride(node, override) - applies the given
261
+ * override to node. Should check whether the given node doesn't
262
+ * already provide its own override, in which case the given
263
+ * override should not be applied. May perform mutations as
264
+ * explained above.
243
265
  *
244
- * @param hasContext(node). Returns true if the given node
245
- * already provides the context to set.
266
+ * hasContext(node) - returns true if the given node
267
+ * already provides the context to set.
246
268
  *
247
- * @param setContext(node, hasOverrideAncestor). Applies the context
248
- * to the given node. Should clear overrides recursively. Should
249
- * also clear context recursively to avoid unnecessarily nested
250
- * contexts. hasOverrideAncestor is true if an override is in effect
251
- * above the given node (see explanation above). May perform
252
- * mutations as explained above.
269
+ * setContext(node, hasOverrideAncestor) - applies the context
270
+ * to the given node. Should clear overrides recursively. Should
271
+ * also clear context recursively to avoid unnecessarily nested
272
+ * contexts. hasOverrideAncestor is true if an override is in effect
273
+ * above the given node (see explanation above). May perform
274
+ * mutations as explained above.
253
275
  */
254
- function mutate(liveRange, isUpperBoundary, getOverride, clearOverride, clearOverrideRec, pushDownOverride, hasContext, setContext, rootHasContext) {
276
+ function mutate(liveRange, formatter, rootHasImpliedContext) {
255
277
  if (liveRange.collapsed) {
256
278
  return;
257
279
  }
@@ -262,27 +284,90 @@ define([
262
284
  var bottommostOverrideNode = null;
263
285
  var isNonClearableOverride = false;
264
286
  var upperBoundaryAndBeyond = false;
265
- var fromCacToContext = Dom.childAndParentsUntilIncl(cac, hasContext);
287
+ var fromCacToContext = Dom.childAndParentsUntilIncl(cac, function (node) {
288
+ // Because we shouldn't expect hasContext to handle the
289
+ // document element (which has nodeType 9).
290
+ return !node.parentNode || 9 === node.parentNode.nodeType || formatter.hasContext(node);
291
+ });
266
292
  Arrays.forEach(fromCacToContext, function (node) {
267
- upperBoundaryAndBeyond = upperBoundaryAndBeyond || isUpperBoundary(node);
268
- if (getOverride(node)) {
293
+ upperBoundaryAndBeyond = upperBoundaryAndBeyond || formatter.isUpperBoundary(node);
294
+ if (null != formatter.getOverride(node)) {
269
295
  topmostOverrideNode = node;
270
296
  isNonClearableOverride = upperBoundaryAndBeyond;
271
297
  bottommostOverrideNode = bottommostOverrideNode || node;
272
298
  }
273
299
  });
274
- if ((rootHasContext || hasContext(fromCacToContext[fromCacToContext.length - 1]))
300
+ if ((rootHasImpliedContext || formatter.hasContext(Arrays.last(fromCacToContext)))
275
301
  && !isNonClearableOverride) {
276
302
  var pushDownFrom = topmostOverrideNode || cac;
277
- var cacOverride = getOverride(bottommostOverrideNode || cac);
278
- pushDownContext(liveRange, pushDownFrom, cacOverride, getOverride, clearOverride, clearOverrideRec, pushDownOverride);
303
+ var cacOverride = formatter.getOverride(bottommostOverrideNode || cac);
304
+ var clearOverrideRec = formatter.clearOverrideRec || function (node) {
305
+ Dom.walkRec(node, formatter.clearOverride);
306
+ };
307
+ pushDownContext(
308
+ liveRange,
309
+ pushDownFrom,
310
+ cacOverride,
311
+ formatter.getOverride,
312
+ formatter.clearOverride,
313
+ clearOverrideRec,
314
+ formatter.pushDownOverride
315
+ );
279
316
  } else {
280
- walkBoundary(liveRange, getOverride, pushDownOverride, clearOverride, function (node) {
281
- return setContext(node, isNonClearableOverride);
282
- });
317
+ var setContext = function (node) {
318
+ formatter.setContext(node, isNonClearableOverride);
319
+ };
320
+ walkBoundary(
321
+ liveRange,
322
+ formatter.getOverride,
323
+ formatter.pushDownOverride,
324
+ formatter.clearOverride,
325
+ setContext
326
+ );
283
327
  }
284
328
  }
285
329
 
330
+ function fixupRange(liveRange, mutate) {
331
+ // Because we are mutating the range several times and don't
332
+ // want the caller to see the in-between updates, and because we
333
+ // are using trimRange() below to adjust the range's boundary
334
+ // points, which we don't want the browser to re-adjust (which
335
+ // some browsers do).
336
+ var range = Dom.stableRange(liveRange);
337
+
338
+ // Because we should avoid splitTextContainers() if this call is a noop.
339
+ if (range.collapsed) {
340
+ return;
341
+ }
342
+
343
+ // Because trimRangeClosingOpening(), mutate() and
344
+ // adjustPointMoveBackWithinRange() require boundary points to
345
+ // be between nodes.
346
+ Dom.splitTextContainers(range);
347
+
348
+ // Because we want unbolding
349
+ // <b>one<i>two{</i>three}</b>
350
+ // to result in
351
+ // <b>one<i>two</i></b>three
352
+ // and not in
353
+ // <b>one</b><i><b>two</b></i>three
354
+ // and because adjustPointMoveBackWithinRange() requires the
355
+ // left boundary point to be next to a non-ignorable node.
356
+ Dom.trimRangeClosingOpening(range, Html.isIgnorableWhitespace);
357
+
358
+ // Because mutation needs to keep track and adjust boundary
359
+ // points.
360
+ var leftPoint = Dom.cursorFromBoundaryPoint(range.startContainer, range.startOffset);
361
+ var rightPoint = Dom.cursorFromBoundaryPoint(range.endContainer, range.endOffset);
362
+
363
+ mutate(range, leftPoint, rightPoint);
364
+
365
+ // Because we must reflect the adjusted boundary points in the
366
+ // given range.
367
+ Dom.setRangeStartFromCursor(liveRange, leftPoint);
368
+ Dom.setRangeEndFromCursor(liveRange, rightPoint);
369
+ }
370
+
286
371
  function adjustPointShallowRemove(point, left, node) {
287
372
  if (point.node === node) {
288
373
  point.next();
@@ -314,15 +399,15 @@ define([
314
399
  }
315
400
  }
316
401
 
317
- function shallowRemoveAdjust(node, leftPoint, rightPoint) {
402
+ function removeShallowAdjust(node, leftPoint, rightPoint) {
318
403
  adjustPointShallowRemove(leftPoint, true, node);
319
404
  adjustPointShallowRemove(rightPoint, false, node);
320
- Dom.shallowRemove(node);
405
+ Dom.removeShallow(node);
321
406
  }
322
407
 
323
408
  function wrapAdjust(node, wrapper, leftPoint, rightPoint) {
324
409
  if (wrapper.parentNode) {
325
- shallowRemoveAdjust(wrapper, leftPoint, rightPoint);
410
+ removeShallowAdjust(wrapper, leftPoint, rightPoint);
326
411
  }
327
412
  adjustPointWrap(leftPoint, true, node, wrapper);
328
413
  adjustPointWrap(rightPoint, false, node, wrapper);
@@ -335,21 +420,15 @@ define([
335
420
  Dom.insert(node, ref, atEnd);
336
421
  }
337
422
 
338
- function nextSibling(node) {
339
- return node.nextSibling;
340
- }
341
-
342
- // TODO when restacking the <b> that wraps "z" in
343
- // <u><b>x</b><s><b>z</b></s></u>, join with the <b> that wraps "x".
344
423
  function restackRec(node, hasContext, notIgnoreHorizontal, notIgnoreVertical) {
345
424
  if (1 !== node.nodeType || notIgnoreVertical(node)) {
346
425
  return null;
347
426
  }
348
- var maybeContext = Dom.walkUntil(node.firstChild, nextSibling, notIgnoreHorizontal);
427
+ var maybeContext = Dom.next(node.firstChild, notIgnoreHorizontal);
349
428
  if (!maybeContext) {
350
429
  return null;
351
430
  }
352
- var notIgnorable = Dom.walkUntil(maybeContext.nextSibling, nextSibling, notIgnoreHorizontal);
431
+ var notIgnorable = Dom.next(maybeContext.nextSibling, notIgnoreHorizontal);
353
432
  if (notIgnorable) {
354
433
  return null;
355
434
  }
@@ -375,154 +454,323 @@ define([
375
454
  return true;
376
455
  }
377
456
 
378
- function format(liveRange, nodeName, unformat) {
379
- var leftPoint;
380
- var rightPoint;
457
+ function ensureWrapper(node, nodeName, hasWrapper, leftPoint, rightPoint) {
458
+ if (node.previousSibling && !hasWrapper(node.previousSibling)) {
459
+ // Because restacking here solves two problems: one the
460
+ // case where the context was unnecessarily pushed down
461
+ // on the left of the range, and two to join with a
462
+ // context node that already exists to the left of the
463
+ // range.
464
+ restack(node.previousSibling,
465
+ hasWrapper,
466
+ Html.isIgnorableWhitespace,
467
+ Html.isInlineFormattable,
468
+ leftPoint,
469
+ rightPoint);
470
+ }
471
+ if (node.previousSibling && hasWrapper(node.previousSibling)) {
472
+ insertAdjust(node, node.previousSibling, true, leftPoint, rightPoint);
473
+ return true;
474
+ }
475
+ if (!hasWrapper(node)) {
476
+ var wrapper = document.createElement(nodeName);
477
+ wrapAdjust(node, wrapper, leftPoint, rightPoint);
478
+ return true;
479
+ }
480
+ return false;
481
+ }
482
+
483
+ function isUpperBoundary_default(node) {
484
+ // Because the body element is an obvious upper boundary, and
485
+ // because, when we are inside an editable, we shouldn't make
486
+ // modifications outside the editable (if we are not inside
487
+ // an editable, we don't care).
488
+ return 'BODY' === node.nodeName || Html.isEditingHost(node);
489
+ }
381
490
 
491
+ function makeNodeFormatter(nodeName, leftPoint, rightPoint) {
382
492
  function hasContext(node) {
383
- if (unformat) {
384
- // Because we pass rootHasContext=true to mutate.
385
- return false;
386
- }
387
493
  return nodeName === node.nodeName;
388
494
  }
389
495
 
390
- function getOverride(node) {
391
- if (!unformat) {
392
- return false;
496
+ function clearContext(node) {
497
+ if (nodeName === node.nodeName) {
498
+ removeShallowAdjust(node, leftPoint, rightPoint);
393
499
  }
394
- return nodeName === node.nodeName;
395
500
  }
396
501
 
397
- function hasOverride(node) {
398
- return !!getOverride(node);
502
+ function clearContextRec(node) {
503
+ Dom.walkRec(node, clearContext);
399
504
  }
400
505
 
401
- function clearOverride(node) {
402
- var next = node.nextSibling;
403
- if (unformat && nodeName === node.nodeName) {
404
- shallowRemoveAdjust(node, leftPoint, rightPoint);
506
+ function setContext(node) {
507
+ if (ensureWrapper(node, nodeName, hasContext, leftPoint, rightPoint)) {
508
+ // Because the node was wrapped with a context, and if
509
+ // the node itself has the context, it should be cleared
510
+ // to avoid nested contexts.
511
+ clearContextRec(node);
512
+ } else {
513
+ // Because the node itself has the context and was not
514
+ // wrapped, we must only clear its children.
515
+ Dom.walk(node.firstChild, clearContextRec);
405
516
  }
406
- return next;
407
517
  }
408
518
 
409
- function clearOverrideRec(node) {
410
- return Dom.walkRec(node, clearOverride);
411
- }
519
+ return {
520
+ hasContext: hasContext,
521
+ getOverride: Fn.noop,
522
+ clearOverride: Fn.noop,
523
+ pushDownOverride: Fn.noop,
524
+ setContext: setContext,
525
+ isUpperBoundary: isUpperBoundary_default
526
+ };
527
+ }
412
528
 
413
- function clearContext(node) {
414
- var next = node.nextSibling;
415
- if (!unformat && nodeName === node.nodeName) {
416
- shallowRemoveAdjust(node, leftPoint, rightPoint);
417
- }
418
- return next;
529
+ function makeNodeUnformatter(nodeName, leftPoint, rightPoint) {
530
+
531
+ function getOverride(node) {
532
+ return nodeName === node.nodeName ? true : null;
419
533
  }
420
534
 
421
- function clearContextRec(node) {
422
- return Dom.walkRec(node, clearContext);
423
- }
424
-
425
- function ensureWrapper(node, hasWrapper) {
426
- if (node.previousSibling && !hasWrapper(node.previousSibling)) {
427
- // Because restacking here solves two problems: one the
428
- // case where the context was unnecessarily pushed down
429
- // on the left of the range, and two to join with a
430
- // context node that already exists to the left of the
431
- // range.
432
- restack(node.previousSibling,
433
- hasWrapper,
434
- Html.isIgnorableWhitespace,
435
- Html.isInlineFormattable,
436
- leftPoint, rightPoint);
437
- }
438
- if (node.previousSibling && hasWrapper(node.previousSibling)) {
439
- insertAdjust(node, node.previousSibling, true, leftPoint, rightPoint);
440
- return true;
441
- } else if (!hasWrapper(node)) {
442
- var wrapper = document.createElement(nodeName);
443
- wrapAdjust(node, wrapper, leftPoint, rightPoint);
444
- return true;
535
+ function clearOverride(node) {
536
+ if (nodeName === node.nodeName) {
537
+ removeShallowAdjust(node, leftPoint, rightPoint);
445
538
  }
446
- return false;
447
539
  }
448
540
 
449
541
  function pushDownOverride(node, override) {
450
542
  if (!override) {
451
- return node.nextSibling;
543
+ return;
452
544
  }
453
- if (!unformat) {
454
- throw "not implemented";
545
+ ensureWrapper(node, nodeName, getOverride, leftPoint, rightPoint);
546
+ }
547
+
548
+ return {
549
+ hasContext: Fn.returnFalse,
550
+ setContext: Fn.noop,
551
+ getOverride: getOverride,
552
+ clearOverride: clearOverride,
553
+ pushDownOverride: pushDownOverride,
554
+ isUpperBoundary: isUpperBoundary_default
555
+ };
556
+ }
557
+
558
+ function createStyleWrapper_default() {
559
+ return document.createElement('SPAN');
560
+ }
561
+
562
+ function isStyleEq_default(styleValueA, styleValueB) {
563
+ return styleValueA === styleValueB;
564
+ }
565
+
566
+ function isStyleWrapperReusable_default(node) {
567
+ return 'SPAN' === node.nodeName;
568
+ }
569
+
570
+ function isStyleWrapperPrunable_default(node) {
571
+ return ('SPAN' === node.nodeName
572
+ && Arrays.every(Arrays.map(Dom.attrs(node), Arrays.second),
573
+ Strings.empty));
574
+ }
575
+
576
+ function makeStyleFormatter(styleName, styleValue, createWrapper, isStyleEq, isReusable, isPrunable, leftPoint, rightPoint) {
577
+
578
+ function removeStyle(node, styleName) {
579
+ if (Strings.empty(Dom.getStyle(node, styleName))) {
580
+ return;
581
+ }
582
+ Dom.setStyle(node, styleName, null);
583
+ if (isPrunable(node)) {
584
+ removeShallowAdjust(node, leftPoint, rightPoint);
455
585
  }
456
- var next = node.nextSibling;
457
- ensureWrapper(node, hasOverride);
458
- return next;
459
586
  }
460
587
 
461
- function setContext(node) {
462
- if (unformat) {
463
- throw "not implemented";
588
+ function setStyle(node, styleName, styleValue, prevWrapper) {
589
+ if (prevWrapper && prevWrapper === node.previousSibling) {
590
+ insertAdjust(node, prevWrapper, true, leftPoint, rightPoint);
591
+ removeStyle(node, styleName);
592
+ return prevWrapper;
464
593
  }
465
- var next = node.nextSibling;
466
- if (ensureWrapper(node, hasContext)) {
467
- // Because the node was wrapped with a context, and if
468
- // the node itself has the context, it should be cleared
469
- // to avoid nested contexts.
470
- clearContextRec(node);
471
- } else {
472
- // Because the node itself has the context and was not
473
- // wrapped, we must only clear its children.
474
- Dom.walk(node.firstChild, clearContextRec);
594
+ if (isReusable(node)) {
595
+ Dom.setStyle(node, styleName, styleValue);
596
+ return prevWrapper;
475
597
  }
476
- clearOverrideRec(node);
477
- return next;
598
+ var wrapper = createWrapper();
599
+ Dom.setStyle(wrapper, styleName, styleValue);
600
+ wrapAdjust(node, wrapper, leftPoint, rightPoint);
601
+ removeStyle(node, styleName);
602
+ return wrapper;
478
603
  }
479
604
 
480
- function isUpperBoundary(node) {
481
- return 'BODY' === node.nodeName;
605
+ function hasContext(node) {
606
+ return isStyleEq(Dom.getStyle(node, styleName), styleValue);
482
607
  }
483
608
 
484
- // Because we are mutating the range several times and don't
485
- // want the caller to see the in-between updates, and because we
486
- // are using trimRange() below to adjust the range's boundary
487
- // points, which we don't want the browser to re-adjust (which
488
- // some browsers do).
489
- var range = Dom.stableRange(liveRange);
609
+ function getOverride(node) {
610
+ var override = Dom.getStyle(node, styleName);
611
+ return (Strings.empty(override) || isStyleEq(override, styleValue)
612
+ ? null
613
+ : override);
614
+ }
490
615
 
491
- // Because we should avoid splitTextContainers() if this call is a noop.
492
- if (range.collapsed) {
493
- return;
616
+ function clearOverride(node) {
617
+ removeStyle(node, styleName);
494
618
  }
495
619
 
496
- // Because trimRangeClosingOpening(), mutate() and
497
- // adjustPointMoveBackWithinRange() require boundary points to
498
- // be between nodes.
499
- Dom.splitTextContainers(range);
620
+ function clearOverrideRec(node) {
621
+ Dom.walkRec(node, clearOverride);
622
+ }
500
623
 
501
- // Because we want unbolding
502
- // <b>one<i>two{</i>three}</b>
503
- // to result in
504
- // <b>one<i>two</i></b>three
505
- // and not in
506
- // <b>one</b><i><b>two</b></i>three
507
- // and because adjustPointMoveBackWithinRange() requires the
508
- // left boundary point to be next to a non-ignorable node.
509
- Dom.trimRangeClosingOpening(range, Html.isIgnorableWhitespace);
624
+ var overrideWrapper = null;
625
+ function pushDownOverride(node, override) {
626
+ if (Strings.empty(override) || !Strings.empty(Dom.getStyle(node, styleName))) {
627
+ return;
628
+ }
629
+ overrideWrapper = setStyle(node, styleName, override, overrideWrapper);
630
+ }
510
631
 
511
- // Because mutation needs to keep track and adjust boundary
512
- // points.
513
- leftPoint = Dom.cursorFromBoundaryPoint(range.startContainer, range.startOffset);
514
- rightPoint = Dom.cursorFromBoundaryPoint(range.endContainer, range.endOffset);
632
+ var contextWrapper = null;
633
+ function setContext(node) {
634
+ Dom.walk(node.firstChild, clearOverrideRec);
635
+ contextWrapper = setStyle(node, styleName, styleValue, contextWrapper);
636
+ }
515
637
 
516
- mutate(range, isUpperBoundary, getOverride, clearOverride, clearOverrideRec, pushDownOverride, hasContext, setContext, unformat);
638
+ return {
639
+ hasContext: hasContext,
640
+ getOverride: getOverride,
641
+ clearOverride: clearOverride,
642
+ pushDownOverride: pushDownOverride,
643
+ setContext: setContext,
644
+ isUpperBoundary: isUpperBoundary_default
645
+ };
646
+ }
517
647
 
518
- // Because we must reflect the adjusted boundary points in the
519
- // given range.
520
- Dom.setRangeStartFromCursor(liveRange, leftPoint);
521
- Dom.setRangeEndFromCursor(liveRange, rightPoint);
648
+ function format(liveRange, nodeName) {
649
+ fixupRange(liveRange, function (range, leftPoint, rightPoint) {
650
+ mutate(range, makeNodeFormatter(nodeName, leftPoint, rightPoint));
651
+ });
652
+ }
653
+
654
+ function unformat(liveRange, nodeName) {
655
+ fixupRange(liveRange, function (range, leftPoint, rightPoint) {
656
+ mutate(range, makeNodeUnformatter(nodeName, leftPoint, rightPoint), true);
657
+ });
658
+ }
659
+
660
+ function findReusableAncestor(range, hasContext, getOverride, isUpperBoundary, isReusable) {
661
+ var obstruction = null;
662
+ function untilIncl(node) {
663
+ return (null != getOverride(node)
664
+ || hasContext(node)
665
+ || isReusable(node)
666
+ || isUpperBoundary(node));
667
+ }
668
+ function beforeAfter(node) {
669
+ obstruction = obstruction || !Html.isIgnorableWhitespace(node);
670
+ }
671
+ var start = Dom.nodeAtOffset(range.startContainer, range.startOffset);
672
+ var end = Dom.nodeAtOffset(range.endContainer, range.endOffset);
673
+ var startEnd = Dom.isAtEnd(range.startContainer, range.startOffset);
674
+ var endEnd = Dom.isAtEnd(range.endContainer, range.endOffset);
675
+ var ascStart = Dom.childAndParentsUntilIncl(start, untilIncl);
676
+ var ascEnd = Dom.childAndParentsUntilIncl(end, untilIncl);
677
+ var reusable = Arrays.last(ascStart);
678
+ function at(node) {
679
+ // Because the start node is inside the range.
680
+ if (node === start && !startEnd) {
681
+ return;
682
+ }
683
+ // Because the end node is outside the range.
684
+ if (node === end && !endEnd) {
685
+ beforeAfter(node);
686
+ return;
687
+ }
688
+ obstruction = obstruction || !Html.isInlineFormattable(node);
689
+ }
690
+ if (!reusable || !isReusable(reusable) || reusable !== Arrays.last(ascEnd)) {
691
+ return null;
692
+ }
693
+ ascendWalkSiblings(ascStart, startEnd, Fn.noop, beforeAfter, at, Fn.noop);
694
+ if (obstruction) {
695
+ return null;
696
+ }
697
+ ascendWalkSiblings(ascEnd, endEnd, Fn.noop, Fn.noop, at, beforeAfter);
698
+ if (obstruction) {
699
+ return null;
700
+ }
701
+ return reusable;
702
+ }
703
+
704
+ function formatStyle(liveRange, styleName, styleValue, createWrapper, isStyleEq, isReusable, isPrunable) {
705
+ createWrapper = createWrapper || createStyleWrapper_default;
706
+ isStyleEq = isStyleEq || isStyleEq_default;
707
+ isReusable = isReusable || isStyleWrapperReusable_default;
708
+ isPrunable = isPrunable || isStyleWrapperPrunable_default;
709
+ fixupRange(liveRange, function (range, leftPoint, rightPoint) {
710
+ var formatter = makeStyleFormatter(
711
+ styleName,
712
+ styleValue,
713
+ createWrapper,
714
+ isStyleEq,
715
+ isReusable,
716
+ isPrunable,
717
+ leftPoint,
718
+ rightPoint
719
+ );
720
+ var reusableAncestor = findReusableAncestor(
721
+ range,
722
+ formatter.hasContext,
723
+ formatter.getOverride,
724
+ formatter.isUpperBoundary,
725
+ isReusable
726
+ );
727
+ if (reusableAncestor) {
728
+ formatter.setContext(reusableAncestor);
729
+ } else {
730
+ mutate(range, formatter, false);
731
+ }
732
+ });
733
+ }
734
+
735
+ function splitBoundary(liveRange, pred, clone) {
736
+ clone = clone || Dom.cloneShallow;
737
+ fixupRange(liveRange, function (range, leftPoint, rightPoint) {
738
+
739
+ var wrapper = null;
740
+
741
+ function carryDown(elem, stop) {
742
+ return stop || !pred(elem);
743
+ }
744
+
745
+ function pushDown(node, stop) {
746
+ if (stop) {
747
+ return;
748
+ }
749
+ if (!wrapper || node.parentNode.previousSibling !== wrapper) {
750
+ wrapper = clone(node.parentNode);
751
+ insertAdjust(wrapper, node.parentNode, false, leftPoint, rightPoint);
752
+ }
753
+ insertAdjust(node, wrapper, true, leftPoint, rightPoint);
754
+ }
755
+
756
+ var sc = range.startContainer;
757
+ var so = range.startOffset;
758
+ var ec = range.endContainer;
759
+ var eo = range.endOffset;
760
+ var cac = range.commonAncestorContainer;
761
+ var startEnd = Dom.isAtEnd(sc, so);
762
+ var endEnd = Dom.isAtEnd(ec, eo);
763
+ var ascStart = Dom.childAndParentsUntilNode(Dom.nodeAtOffset(sc, so), cac);
764
+ var ascEnd = Dom.childAndParentsUntilNode(Dom.nodeAtOffset(ec, eo), cac);
765
+ ascendWalkSiblings(ascStart, startEnd, carryDown, pushDown, Fn.noop, Fn.noop, null);
766
+ ascendWalkSiblings(ascEnd, endEnd, carryDown, pushDown, Fn.noop, Fn.noop, null);
767
+ });
522
768
  }
523
769
 
524
770
  return {
525
- mutate: mutate,
526
- format: format
771
+ format: format,
772
+ unformat: unformat,
773
+ formatStyle: formatStyle,
774
+ splitBoundary: splitBoundary
527
775
  };
528
776
  });