locomotive-aloha-rails 0.23.2.1 → 0.23.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
  });