Property changes on: trunk/extensions/VisualEditor/contentEditable/main.js |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 1 | + native |
Index: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-selectionsaverestore.js |
— | — | @@ -1,195 +1,195 @@ |
2 | | -/**
|
3 | | - * @license Selection save and restore module for Rangy.
|
4 | | - * Saves and restores user selections using marker invisible elements in the DOM.
|
5 | | - *
|
6 | | - * Part of Rangy, a cross-browser JavaScript range and selection library
|
7 | | - * http://code.google.com/p/rangy/
|
8 | | - *
|
9 | | - * Depends on Rangy core.
|
10 | | - *
|
11 | | - * Copyright 2011, Tim Down
|
12 | | - * Licensed under the MIT license.
|
13 | | - * Version: 1.2.2
|
14 | | - * Build date: 13 November 2011
|
15 | | - */
|
16 | | -rangy.createModule("SaveRestore", function(api, module) {
|
17 | | - api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
|
18 | | -
|
19 | | - var dom = api.dom;
|
20 | | -
|
21 | | - var markerTextChar = "\ufeff";
|
22 | | -
|
23 | | - function gEBI(id, doc) {
|
24 | | - return (doc || document).getElementById(id);
|
25 | | - }
|
26 | | -
|
27 | | - function insertRangeBoundaryMarker(range, atStart) {
|
28 | | - var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
|
29 | | - var markerEl;
|
30 | | - var doc = dom.getDocument(range.startContainer);
|
31 | | -
|
32 | | - // Clone the Range and collapse to the appropriate boundary point
|
33 | | - var boundaryRange = range.cloneRange();
|
34 | | - boundaryRange.collapse(atStart);
|
35 | | -
|
36 | | - // Create the marker element containing a single invisible character using DOM methods and insert it
|
37 | | - markerEl = doc.createElement("span");
|
38 | | - markerEl.id = markerId;
|
39 | | - markerEl.style.lineHeight = "0";
|
40 | | - markerEl.style.display = "none";
|
41 | | - markerEl.className = "rangySelectionBoundary";
|
42 | | - markerEl.appendChild(doc.createTextNode(markerTextChar));
|
43 | | -
|
44 | | - boundaryRange.insertNode(markerEl);
|
45 | | - boundaryRange.detach();
|
46 | | - return markerEl;
|
47 | | - }
|
48 | | -
|
49 | | - function setRangeBoundary(doc, range, markerId, atStart) {
|
50 | | - var markerEl = gEBI(markerId, doc);
|
51 | | - if (markerEl) {
|
52 | | - range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
|
53 | | - markerEl.parentNode.removeChild(markerEl);
|
54 | | - } else {
|
55 | | - module.warn("Marker element has been removed. Cannot restore selection.");
|
56 | | - }
|
57 | | - }
|
58 | | -
|
59 | | - function compareRanges(r1, r2) {
|
60 | | - return r2.compareBoundaryPoints(r1.START_TO_START, r1);
|
61 | | - }
|
62 | | -
|
63 | | - function saveSelection(win) {
|
64 | | - win = win || window;
|
65 | | - var doc = win.document;
|
66 | | - if (!api.isSelectionValid(win)) {
|
67 | | - module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
|
68 | | - return;
|
69 | | - }
|
70 | | - var sel = api.getSelection(win);
|
71 | | - var ranges = sel.getAllRanges();
|
72 | | - var rangeInfos = [], startEl, endEl, range;
|
73 | | -
|
74 | | - // Order the ranges by position within the DOM, latest first
|
75 | | - ranges.sort(compareRanges);
|
76 | | -
|
77 | | - for (var i = 0, len = ranges.length; i < len; ++i) {
|
78 | | - range = ranges[i];
|
79 | | - if (range.collapsed) {
|
80 | | - endEl = insertRangeBoundaryMarker(range, false);
|
81 | | - rangeInfos.push({
|
82 | | - markerId: endEl.id,
|
83 | | - collapsed: true
|
84 | | - });
|
85 | | - } else {
|
86 | | - endEl = insertRangeBoundaryMarker(range, false);
|
87 | | - startEl = insertRangeBoundaryMarker(range, true);
|
88 | | -
|
89 | | - rangeInfos[i] = {
|
90 | | - startMarkerId: startEl.id,
|
91 | | - endMarkerId: endEl.id,
|
92 | | - collapsed: false,
|
93 | | - backwards: ranges.length == 1 && sel.isBackwards()
|
94 | | - };
|
95 | | - }
|
96 | | - }
|
97 | | -
|
98 | | - // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
|
99 | | - // between its markers
|
100 | | - for (i = len - 1; i >= 0; --i) {
|
101 | | - range = ranges[i];
|
102 | | - if (range.collapsed) {
|
103 | | - range.collapseBefore(gEBI(rangeInfos[i].markerId, doc));
|
104 | | - } else {
|
105 | | - range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
|
106 | | - range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
|
107 | | - }
|
108 | | - }
|
109 | | -
|
110 | | - // Ensure current selection is unaffected
|
111 | | - sel.setRanges(ranges);
|
112 | | - return {
|
113 | | - win: win,
|
114 | | - doc: doc,
|
115 | | - rangeInfos: rangeInfos,
|
116 | | - restored: false
|
117 | | - };
|
118 | | - }
|
119 | | -
|
120 | | - function restoreSelection(savedSelection, preserveDirection) {
|
121 | | - if (!savedSelection.restored) {
|
122 | | - var rangeInfos = savedSelection.rangeInfos;
|
123 | | - var sel = api.getSelection(savedSelection.win);
|
124 | | - var ranges = [];
|
125 | | -
|
126 | | - // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
|
127 | | - // normalization affecting previously restored ranges.
|
128 | | - for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) {
|
129 | | - rangeInfo = rangeInfos[i];
|
130 | | - range = api.createRange(savedSelection.doc);
|
131 | | - if (rangeInfo.collapsed) {
|
132 | | - var markerEl = gEBI(rangeInfo.markerId, savedSelection.doc);
|
133 | | - if (markerEl) {
|
134 | | - markerEl.style.display = "inline";
|
135 | | - var previousNode = markerEl.previousSibling;
|
136 | | -
|
137 | | - // Workaround for issue 17
|
138 | | - if (previousNode && previousNode.nodeType == 3) {
|
139 | | - markerEl.parentNode.removeChild(markerEl);
|
140 | | - range.collapseToPoint(previousNode, previousNode.length);
|
141 | | - } else {
|
142 | | - range.collapseBefore(markerEl);
|
143 | | - markerEl.parentNode.removeChild(markerEl);
|
144 | | - }
|
145 | | - } else {
|
146 | | - module.warn("Marker element has been removed. Cannot restore selection.");
|
147 | | - }
|
148 | | - } else {
|
149 | | - setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true);
|
150 | | - setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false);
|
151 | | - }
|
152 | | -
|
153 | | - // Normalizing range boundaries is only viable if the selection contains only one range. For example,
|
154 | | - // if the selection contained two ranges that were both contained within the same single text node,
|
155 | | - // both would alter the same text node when restoring and break the other range.
|
156 | | - if (len == 1) {
|
157 | | - range.normalizeBoundaries();
|
158 | | - }
|
159 | | - ranges[i] = range;
|
160 | | - }
|
161 | | - if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) {
|
162 | | - sel.removeAllRanges();
|
163 | | - sel.addRange(ranges[0], true);
|
164 | | - } else {
|
165 | | - sel.setRanges(ranges);
|
166 | | - }
|
167 | | -
|
168 | | - savedSelection.restored = true;
|
169 | | - }
|
170 | | - }
|
171 | | -
|
172 | | - function removeMarkerElement(doc, markerId) {
|
173 | | - var markerEl = gEBI(markerId, doc);
|
174 | | - if (markerEl) {
|
175 | | - markerEl.parentNode.removeChild(markerEl);
|
176 | | - }
|
177 | | - }
|
178 | | -
|
179 | | - function removeMarkers(savedSelection) {
|
180 | | - var rangeInfos = savedSelection.rangeInfos;
|
181 | | - for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
|
182 | | - rangeInfo = rangeInfos[i];
|
183 | | - if (rangeInfo.collapsed) {
|
184 | | - removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
|
185 | | - } else {
|
186 | | - removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
|
187 | | - removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
|
188 | | - }
|
189 | | - }
|
190 | | - }
|
191 | | -
|
192 | | - api.saveSelection = saveSelection;
|
193 | | - api.restoreSelection = restoreSelection;
|
194 | | - api.removeMarkerElement = removeMarkerElement;
|
195 | | - api.removeMarkers = removeMarkers;
|
196 | | -});
|
| 2 | +/** |
| 3 | + * @license Selection save and restore module for Rangy. |
| 4 | + * Saves and restores user selections using marker invisible elements in the DOM. |
| 5 | + * |
| 6 | + * Part of Rangy, a cross-browser JavaScript range and selection library |
| 7 | + * http://code.google.com/p/rangy/ |
| 8 | + * |
| 9 | + * Depends on Rangy core. |
| 10 | + * |
| 11 | + * Copyright 2011, Tim Down |
| 12 | + * Licensed under the MIT license. |
| 13 | + * Version: 1.2.2 |
| 14 | + * Build date: 13 November 2011 |
| 15 | + */ |
| 16 | +rangy.createModule("SaveRestore", function(api, module) { |
| 17 | + api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] ); |
| 18 | + |
| 19 | + var dom = api.dom; |
| 20 | + |
| 21 | + var markerTextChar = "\ufeff"; |
| 22 | + |
| 23 | + function gEBI(id, doc) { |
| 24 | + return (doc || document).getElementById(id); |
| 25 | + } |
| 26 | + |
| 27 | + function insertRangeBoundaryMarker(range, atStart) { |
| 28 | + var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2); |
| 29 | + var markerEl; |
| 30 | + var doc = dom.getDocument(range.startContainer); |
| 31 | + |
| 32 | + // Clone the Range and collapse to the appropriate boundary point |
| 33 | + var boundaryRange = range.cloneRange(); |
| 34 | + boundaryRange.collapse(atStart); |
| 35 | + |
| 36 | + // Create the marker element containing a single invisible character using DOM methods and insert it |
| 37 | + markerEl = doc.createElement("span"); |
| 38 | + markerEl.id = markerId; |
| 39 | + markerEl.style.lineHeight = "0"; |
| 40 | + markerEl.style.display = "none"; |
| 41 | + markerEl.className = "rangySelectionBoundary"; |
| 42 | + markerEl.appendChild(doc.createTextNode(markerTextChar)); |
| 43 | + |
| 44 | + boundaryRange.insertNode(markerEl); |
| 45 | + boundaryRange.detach(); |
| 46 | + return markerEl; |
| 47 | + } |
| 48 | + |
| 49 | + function setRangeBoundary(doc, range, markerId, atStart) { |
| 50 | + var markerEl = gEBI(markerId, doc); |
| 51 | + if (markerEl) { |
| 52 | + range[atStart ? "setStartBefore" : "setEndBefore"](markerEl); |
| 53 | + markerEl.parentNode.removeChild(markerEl); |
| 54 | + } else { |
| 55 | + module.warn("Marker element has been removed. Cannot restore selection."); |
| 56 | + } |
| 57 | + } |
| 58 | + |
| 59 | + function compareRanges(r1, r2) { |
| 60 | + return r2.compareBoundaryPoints(r1.START_TO_START, r1); |
| 61 | + } |
| 62 | + |
| 63 | + function saveSelection(win) { |
| 64 | + win = win || window; |
| 65 | + var doc = win.document; |
| 66 | + if (!api.isSelectionValid(win)) { |
| 67 | + module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."); |
| 68 | + return; |
| 69 | + } |
| 70 | + var sel = api.getSelection(win); |
| 71 | + var ranges = sel.getAllRanges(); |
| 72 | + var rangeInfos = [], startEl, endEl, range; |
| 73 | + |
| 74 | + // Order the ranges by position within the DOM, latest first |
| 75 | + ranges.sort(compareRanges); |
| 76 | + |
| 77 | + for (var i = 0, len = ranges.length; i < len; ++i) { |
| 78 | + range = ranges[i]; |
| 79 | + if (range.collapsed) { |
| 80 | + endEl = insertRangeBoundaryMarker(range, false); |
| 81 | + rangeInfos.push({ |
| 82 | + markerId: endEl.id, |
| 83 | + collapsed: true |
| 84 | + }); |
| 85 | + } else { |
| 86 | + endEl = insertRangeBoundaryMarker(range, false); |
| 87 | + startEl = insertRangeBoundaryMarker(range, true); |
| 88 | + |
| 89 | + rangeInfos[i] = { |
| 90 | + startMarkerId: startEl.id, |
| 91 | + endMarkerId: endEl.id, |
| 92 | + collapsed: false, |
| 93 | + backwards: ranges.length == 1 && sel.isBackwards() |
| 94 | + }; |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie |
| 99 | + // between its markers |
| 100 | + for (i = len - 1; i >= 0; --i) { |
| 101 | + range = ranges[i]; |
| 102 | + if (range.collapsed) { |
| 103 | + range.collapseBefore(gEBI(rangeInfos[i].markerId, doc)); |
| 104 | + } else { |
| 105 | + range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc)); |
| 106 | + range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc)); |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + // Ensure current selection is unaffected |
| 111 | + sel.setRanges(ranges); |
| 112 | + return { |
| 113 | + win: win, |
| 114 | + doc: doc, |
| 115 | + rangeInfos: rangeInfos, |
| 116 | + restored: false |
| 117 | + }; |
| 118 | + } |
| 119 | + |
| 120 | + function restoreSelection(savedSelection, preserveDirection) { |
| 121 | + if (!savedSelection.restored) { |
| 122 | + var rangeInfos = savedSelection.rangeInfos; |
| 123 | + var sel = api.getSelection(savedSelection.win); |
| 124 | + var ranges = []; |
| 125 | + |
| 126 | + // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid |
| 127 | + // normalization affecting previously restored ranges. |
| 128 | + for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) { |
| 129 | + rangeInfo = rangeInfos[i]; |
| 130 | + range = api.createRange(savedSelection.doc); |
| 131 | + if (rangeInfo.collapsed) { |
| 132 | + var markerEl = gEBI(rangeInfo.markerId, savedSelection.doc); |
| 133 | + if (markerEl) { |
| 134 | + markerEl.style.display = "inline"; |
| 135 | + var previousNode = markerEl.previousSibling; |
| 136 | + |
| 137 | + // Workaround for issue 17 |
| 138 | + if (previousNode && previousNode.nodeType == 3) { |
| 139 | + markerEl.parentNode.removeChild(markerEl); |
| 140 | + range.collapseToPoint(previousNode, previousNode.length); |
| 141 | + } else { |
| 142 | + range.collapseBefore(markerEl); |
| 143 | + markerEl.parentNode.removeChild(markerEl); |
| 144 | + } |
| 145 | + } else { |
| 146 | + module.warn("Marker element has been removed. Cannot restore selection."); |
| 147 | + } |
| 148 | + } else { |
| 149 | + setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true); |
| 150 | + setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false); |
| 151 | + } |
| 152 | + |
| 153 | + // Normalizing range boundaries is only viable if the selection contains only one range. For example, |
| 154 | + // if the selection contained two ranges that were both contained within the same single text node, |
| 155 | + // both would alter the same text node when restoring and break the other range. |
| 156 | + if (len == 1) { |
| 157 | + range.normalizeBoundaries(); |
| 158 | + } |
| 159 | + ranges[i] = range; |
| 160 | + } |
| 161 | + if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) { |
| 162 | + sel.removeAllRanges(); |
| 163 | + sel.addRange(ranges[0], true); |
| 164 | + } else { |
| 165 | + sel.setRanges(ranges); |
| 166 | + } |
| 167 | + |
| 168 | + savedSelection.restored = true; |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + function removeMarkerElement(doc, markerId) { |
| 173 | + var markerEl = gEBI(markerId, doc); |
| 174 | + if (markerEl) { |
| 175 | + markerEl.parentNode.removeChild(markerEl); |
| 176 | + } |
| 177 | + } |
| 178 | + |
| 179 | + function removeMarkers(savedSelection) { |
| 180 | + var rangeInfos = savedSelection.rangeInfos; |
| 181 | + for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) { |
| 182 | + rangeInfo = rangeInfos[i]; |
| 183 | + if (rangeInfo.collapsed) { |
| 184 | + removeMarkerElement(savedSelection.doc, rangeInfo.markerId); |
| 185 | + } else { |
| 186 | + removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId); |
| 187 | + removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId); |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + api.saveSelection = saveSelection; |
| 193 | + api.restoreSelection = restoreSelection; |
| 194 | + api.removeMarkerElement = removeMarkerElement; |
| 195 | + api.removeMarkers = removeMarkers; |
| 196 | +}); |
Property changes on: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-selectionsaverestore.js |
___________________________________________________________________ |
Added: svn:eol-style |
197 | 197 | + native |
Index: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-serializer.js |
— | — | @@ -1,300 +1,300 @@ |
2 | | -/**
|
3 | | - * @license Serializer module for Rangy.
|
4 | | - * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
|
5 | | - * cookie or local storage and restore it on the user's next visit to the same page.
|
6 | | - *
|
7 | | - * Part of Rangy, a cross-browser JavaScript range and selection library
|
8 | | - * http://code.google.com/p/rangy/
|
9 | | - *
|
10 | | - * Depends on Rangy core.
|
11 | | - *
|
12 | | - * Copyright 2011, Tim Down
|
13 | | - * Licensed under the MIT license.
|
14 | | - * Version: 1.2.2
|
15 | | - * Build date: 13 November 2011
|
16 | | - */
|
17 | | -rangy.createModule("Serializer", function(api, module) {
|
18 | | - api.requireModules( ["WrappedSelection", "WrappedRange"] );
|
19 | | - var UNDEF = "undefined";
|
20 | | -
|
21 | | - // encodeURIComponent and decodeURIComponent are required for cookie handling
|
22 | | - if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
|
23 | | - module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
|
24 | | - }
|
25 | | -
|
26 | | - // Checksum for checking whether range can be serialized
|
27 | | - var crc32 = (function() {
|
28 | | - function utf8encode(str) {
|
29 | | - var utf8CharCodes = [];
|
30 | | -
|
31 | | - for (var i = 0, len = str.length, c; i < len; ++i) {
|
32 | | - c = str.charCodeAt(i);
|
33 | | - if (c < 128) {
|
34 | | - utf8CharCodes.push(c);
|
35 | | - } else if (c < 2048) {
|
36 | | - utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
|
37 | | - } else {
|
38 | | - utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
|
39 | | - }
|
40 | | - }
|
41 | | - return utf8CharCodes;
|
42 | | - }
|
43 | | -
|
44 | | - var cachedCrcTable = null;
|
45 | | -
|
46 | | - function buildCRCTable() {
|
47 | | - var table = [];
|
48 | | - for (var i = 0, j, crc; i < 256; ++i) {
|
49 | | - crc = i;
|
50 | | - j = 8;
|
51 | | - while (j--) {
|
52 | | - if ((crc & 1) == 1) {
|
53 | | - crc = (crc >>> 1) ^ 0xEDB88320;
|
54 | | - } else {
|
55 | | - crc >>>= 1;
|
56 | | - }
|
57 | | - }
|
58 | | - table[i] = crc >>> 0;
|
59 | | - }
|
60 | | - return table;
|
61 | | - }
|
62 | | -
|
63 | | - function getCrcTable() {
|
64 | | - if (!cachedCrcTable) {
|
65 | | - cachedCrcTable = buildCRCTable();
|
66 | | - }
|
67 | | - return cachedCrcTable;
|
68 | | - }
|
69 | | -
|
70 | | - return function(str) {
|
71 | | - var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
|
72 | | - for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
|
73 | | - y = (crc ^ utf8CharCodes[i]) & 0xFF;
|
74 | | - crc = (crc >>> 8) ^ crcTable[y];
|
75 | | - }
|
76 | | - return (crc ^ -1) >>> 0;
|
77 | | - };
|
78 | | - })();
|
79 | | -
|
80 | | - var dom = api.dom;
|
81 | | -
|
82 | | - function escapeTextForHtml(str) {
|
83 | | - return str.replace(/</g, "<").replace(/>/g, ">");
|
84 | | - }
|
85 | | -
|
86 | | - function nodeToInfoString(node, infoParts) {
|
87 | | - infoParts = infoParts || [];
|
88 | | - var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
|
89 | | - var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
|
90 | | - var start = "", end = "";
|
91 | | - switch (nodeType) {
|
92 | | - case 3: // Text node
|
93 | | - start = escapeTextForHtml(node.nodeValue);
|
94 | | - break;
|
95 | | - case 8: // Comment
|
96 | | - start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
|
97 | | - break;
|
98 | | - default:
|
99 | | - start = "<" + nodeInfo + ">";
|
100 | | - end = "</>";
|
101 | | - break;
|
102 | | - }
|
103 | | - if (start) {
|
104 | | - infoParts.push(start);
|
105 | | - }
|
106 | | - for (var i = 0; i < childCount; ++i) {
|
107 | | - nodeToInfoString(children[i], infoParts);
|
108 | | - }
|
109 | | - if (end) {
|
110 | | - infoParts.push(end);
|
111 | | - }
|
112 | | - return infoParts;
|
113 | | - }
|
114 | | -
|
115 | | - // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
|
116 | | - // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
|
117 | | - // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
|
118 | | - // innerHTML whenever the user changes an input within the element.
|
119 | | - function getElementChecksum(el) {
|
120 | | - var info = nodeToInfoString(el).join("");
|
121 | | - return crc32(info).toString(16);
|
122 | | - }
|
123 | | -
|
124 | | - function serializePosition(node, offset, rootNode) {
|
125 | | - var pathBits = [], n = node;
|
126 | | - rootNode = rootNode || dom.getDocument(node).documentElement;
|
127 | | - while (n && n != rootNode) {
|
128 | | - pathBits.push(dom.getNodeIndex(n, true));
|
129 | | - n = n.parentNode;
|
130 | | - }
|
131 | | - return pathBits.join("/") + ":" + offset;
|
132 | | - }
|
133 | | -
|
134 | | - function deserializePosition(serialized, rootNode, doc) {
|
135 | | - if (rootNode) {
|
136 | | - doc = doc || dom.getDocument(rootNode);
|
137 | | - } else {
|
138 | | - doc = doc || document;
|
139 | | - rootNode = doc.documentElement;
|
140 | | - }
|
141 | | - var bits = serialized.split(":");
|
142 | | - var node = rootNode;
|
143 | | - var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
|
144 | | -
|
145 | | - while (i--) {
|
146 | | - nodeIndex = parseInt(nodeIndices[i], 10);
|
147 | | - if (nodeIndex < node.childNodes.length) {
|
148 | | - node = node.childNodes[parseInt(nodeIndices[i], 10)];
|
149 | | - } else {
|
150 | | - throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
|
151 | | - " has no child with index " + nodeIndex + ", " + i);
|
152 | | - }
|
153 | | - }
|
154 | | -
|
155 | | - return new dom.DomPosition(node, parseInt(bits[1], 10));
|
156 | | - }
|
157 | | -
|
158 | | - function serializeRange(range, omitChecksum, rootNode) {
|
159 | | - rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
|
160 | | - if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
|
161 | | - throw new Error("serializeRange: range is not wholly contained within specified root node");
|
162 | | - }
|
163 | | - var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
|
164 | | - serializePosition(range.endContainer, range.endOffset, rootNode);
|
165 | | - if (!omitChecksum) {
|
166 | | - serialized += "{" + getElementChecksum(rootNode) + "}";
|
167 | | - }
|
168 | | - return serialized;
|
169 | | - }
|
170 | | -
|
171 | | - function deserializeRange(serialized, rootNode, doc) {
|
172 | | - if (rootNode) {
|
173 | | - doc = doc || dom.getDocument(rootNode);
|
174 | | - } else {
|
175 | | - doc = doc || document;
|
176 | | - rootNode = doc.documentElement;
|
177 | | - }
|
178 | | - var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
|
179 | | - var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
|
180 | | - if (checksum && checksum !== getElementChecksum(rootNode)) {
|
181 | | - throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
|
182 | | - ") and target root node (" + rootNodeChecksum + ") do not match");
|
183 | | - }
|
184 | | - var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
|
185 | | - var range = api.createRange(doc);
|
186 | | - range.setStart(start.node, start.offset);
|
187 | | - range.setEnd(end.node, end.offset);
|
188 | | - return range;
|
189 | | - }
|
190 | | -
|
191 | | - function canDeserializeRange(serialized, rootNode, doc) {
|
192 | | - if (rootNode) {
|
193 | | - doc = doc || dom.getDocument(rootNode);
|
194 | | - } else {
|
195 | | - doc = doc || document;
|
196 | | - rootNode = doc.documentElement;
|
197 | | - }
|
198 | | - var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
|
199 | | - var checksum = result[3];
|
200 | | - return !checksum || checksum === getElementChecksum(rootNode);
|
201 | | - }
|
202 | | -
|
203 | | - function serializeSelection(selection, omitChecksum, rootNode) {
|
204 | | - selection = selection || api.getSelection();
|
205 | | - var ranges = selection.getAllRanges(), serializedRanges = [];
|
206 | | - for (var i = 0, len = ranges.length; i < len; ++i) {
|
207 | | - serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
|
208 | | - }
|
209 | | - return serializedRanges.join("|");
|
210 | | - }
|
211 | | -
|
212 | | - function deserializeSelection(serialized, rootNode, win) {
|
213 | | - if (rootNode) {
|
214 | | - win = win || dom.getWindow(rootNode);
|
215 | | - } else {
|
216 | | - win = win || window;
|
217 | | - rootNode = win.document.documentElement;
|
218 | | - }
|
219 | | - var serializedRanges = serialized.split("|");
|
220 | | - var sel = api.getSelection(win);
|
221 | | - var ranges = [];
|
222 | | -
|
223 | | - for (var i = 0, len = serializedRanges.length; i < len; ++i) {
|
224 | | - ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
|
225 | | - }
|
226 | | - sel.setRanges(ranges);
|
227 | | -
|
228 | | - return sel;
|
229 | | - }
|
230 | | -
|
231 | | - function canDeserializeSelection(serialized, rootNode, win) {
|
232 | | - var doc;
|
233 | | - if (rootNode) {
|
234 | | - doc = win ? win.document : dom.getDocument(rootNode);
|
235 | | - } else {
|
236 | | - win = win || window;
|
237 | | - rootNode = win.document.documentElement;
|
238 | | - }
|
239 | | - var serializedRanges = serialized.split("|");
|
240 | | -
|
241 | | - for (var i = 0, len = serializedRanges.length; i < len; ++i) {
|
242 | | - if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
|
243 | | - return false;
|
244 | | - }
|
245 | | - }
|
246 | | -
|
247 | | - return true;
|
248 | | - }
|
249 | | -
|
250 | | -
|
251 | | - var cookieName = "rangySerializedSelection";
|
252 | | -
|
253 | | - function getSerializedSelectionFromCookie(cookie) {
|
254 | | - var parts = cookie.split(/[;,]/);
|
255 | | - for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
|
256 | | - nameVal = parts[i].split("=");
|
257 | | - if (nameVal[0].replace(/^\s+/, "") == cookieName) {
|
258 | | - val = nameVal[1];
|
259 | | - if (val) {
|
260 | | - return decodeURIComponent(val.replace(/\s+$/, ""));
|
261 | | - }
|
262 | | - }
|
263 | | - }
|
264 | | - return null;
|
265 | | - }
|
266 | | -
|
267 | | - function restoreSelectionFromCookie(win) {
|
268 | | - win = win || window;
|
269 | | - var serialized = getSerializedSelectionFromCookie(win.document.cookie);
|
270 | | - if (serialized) {
|
271 | | - deserializeSelection(serialized, win.doc)
|
272 | | - }
|
273 | | - }
|
274 | | -
|
275 | | - function saveSelectionCookie(win, props) {
|
276 | | - win = win || window;
|
277 | | - props = (typeof props == "object") ? props : {};
|
278 | | - var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
|
279 | | - var path = props.path ? ";path=" + props.path : "";
|
280 | | - var domain = props.domain ? ";domain=" + props.domain : "";
|
281 | | - var secure = props.secure ? ";secure" : "";
|
282 | | - var serialized = serializeSelection(api.getSelection(win));
|
283 | | - win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
|
284 | | - }
|
285 | | -
|
286 | | - api.serializePosition = serializePosition;
|
287 | | - api.deserializePosition = deserializePosition;
|
288 | | -
|
289 | | - api.serializeRange = serializeRange;
|
290 | | - api.deserializeRange = deserializeRange;
|
291 | | - api.canDeserializeRange = canDeserializeRange;
|
292 | | -
|
293 | | - api.serializeSelection = serializeSelection;
|
294 | | - api.deserializeSelection = deserializeSelection;
|
295 | | - api.canDeserializeSelection = canDeserializeSelection;
|
296 | | -
|
297 | | - api.restoreSelectionFromCookie = restoreSelectionFromCookie;
|
298 | | - api.saveSelectionCookie = saveSelectionCookie;
|
299 | | -
|
300 | | - api.getElementChecksum = getElementChecksum;
|
301 | | -});
|
| 2 | +/** |
| 3 | + * @license Serializer module for Rangy. |
| 4 | + * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a |
| 5 | + * cookie or local storage and restore it on the user's next visit to the same page. |
| 6 | + * |
| 7 | + * Part of Rangy, a cross-browser JavaScript range and selection library |
| 8 | + * http://code.google.com/p/rangy/ |
| 9 | + * |
| 10 | + * Depends on Rangy core. |
| 11 | + * |
| 12 | + * Copyright 2011, Tim Down |
| 13 | + * Licensed under the MIT license. |
| 14 | + * Version: 1.2.2 |
| 15 | + * Build date: 13 November 2011 |
| 16 | + */ |
| 17 | +rangy.createModule("Serializer", function(api, module) { |
| 18 | + api.requireModules( ["WrappedSelection", "WrappedRange"] ); |
| 19 | + var UNDEF = "undefined"; |
| 20 | + |
| 21 | + // encodeURIComponent and decodeURIComponent are required for cookie handling |
| 22 | + if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) { |
| 23 | + module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method"); |
| 24 | + } |
| 25 | + |
| 26 | + // Checksum for checking whether range can be serialized |
| 27 | + var crc32 = (function() { |
| 28 | + function utf8encode(str) { |
| 29 | + var utf8CharCodes = []; |
| 30 | + |
| 31 | + for (var i = 0, len = str.length, c; i < len; ++i) { |
| 32 | + c = str.charCodeAt(i); |
| 33 | + if (c < 128) { |
| 34 | + utf8CharCodes.push(c); |
| 35 | + } else if (c < 2048) { |
| 36 | + utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128); |
| 37 | + } else { |
| 38 | + utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128); |
| 39 | + } |
| 40 | + } |
| 41 | + return utf8CharCodes; |
| 42 | + } |
| 43 | + |
| 44 | + var cachedCrcTable = null; |
| 45 | + |
| 46 | + function buildCRCTable() { |
| 47 | + var table = []; |
| 48 | + for (var i = 0, j, crc; i < 256; ++i) { |
| 49 | + crc = i; |
| 50 | + j = 8; |
| 51 | + while (j--) { |
| 52 | + if ((crc & 1) == 1) { |
| 53 | + crc = (crc >>> 1) ^ 0xEDB88320; |
| 54 | + } else { |
| 55 | + crc >>>= 1; |
| 56 | + } |
| 57 | + } |
| 58 | + table[i] = crc >>> 0; |
| 59 | + } |
| 60 | + return table; |
| 61 | + } |
| 62 | + |
| 63 | + function getCrcTable() { |
| 64 | + if (!cachedCrcTable) { |
| 65 | + cachedCrcTable = buildCRCTable(); |
| 66 | + } |
| 67 | + return cachedCrcTable; |
| 68 | + } |
| 69 | + |
| 70 | + return function(str) { |
| 71 | + var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable(); |
| 72 | + for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) { |
| 73 | + y = (crc ^ utf8CharCodes[i]) & 0xFF; |
| 74 | + crc = (crc >>> 8) ^ crcTable[y]; |
| 75 | + } |
| 76 | + return (crc ^ -1) >>> 0; |
| 77 | + }; |
| 78 | + })(); |
| 79 | + |
| 80 | + var dom = api.dom; |
| 81 | + |
| 82 | + function escapeTextForHtml(str) { |
| 83 | + return str.replace(/</g, "<").replace(/>/g, ">"); |
| 84 | + } |
| 85 | + |
| 86 | + function nodeToInfoString(node, infoParts) { |
| 87 | + infoParts = infoParts || []; |
| 88 | + var nodeType = node.nodeType, children = node.childNodes, childCount = children.length; |
| 89 | + var nodeInfo = [nodeType, node.nodeName, childCount].join(":"); |
| 90 | + var start = "", end = ""; |
| 91 | + switch (nodeType) { |
| 92 | + case 3: // Text node |
| 93 | + start = escapeTextForHtml(node.nodeValue); |
| 94 | + break; |
| 95 | + case 8: // Comment |
| 96 | + start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->"; |
| 97 | + break; |
| 98 | + default: |
| 99 | + start = "<" + nodeInfo + ">"; |
| 100 | + end = "</>"; |
| 101 | + break; |
| 102 | + } |
| 103 | + if (start) { |
| 104 | + infoParts.push(start); |
| 105 | + } |
| 106 | + for (var i = 0; i < childCount; ++i) { |
| 107 | + nodeToInfoString(children[i], infoParts); |
| 108 | + } |
| 109 | + if (end) { |
| 110 | + infoParts.push(end); |
| 111 | + } |
| 112 | + return infoParts; |
| 113 | + } |
| 114 | + |
| 115 | + // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all |
| 116 | + // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around |
| 117 | + // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's |
| 118 | + // innerHTML whenever the user changes an input within the element. |
| 119 | + function getElementChecksum(el) { |
| 120 | + var info = nodeToInfoString(el).join(""); |
| 121 | + return crc32(info).toString(16); |
| 122 | + } |
| 123 | + |
| 124 | + function serializePosition(node, offset, rootNode) { |
| 125 | + var pathBits = [], n = node; |
| 126 | + rootNode = rootNode || dom.getDocument(node).documentElement; |
| 127 | + while (n && n != rootNode) { |
| 128 | + pathBits.push(dom.getNodeIndex(n, true)); |
| 129 | + n = n.parentNode; |
| 130 | + } |
| 131 | + return pathBits.join("/") + ":" + offset; |
| 132 | + } |
| 133 | + |
| 134 | + function deserializePosition(serialized, rootNode, doc) { |
| 135 | + if (rootNode) { |
| 136 | + doc = doc || dom.getDocument(rootNode); |
| 137 | + } else { |
| 138 | + doc = doc || document; |
| 139 | + rootNode = doc.documentElement; |
| 140 | + } |
| 141 | + var bits = serialized.split(":"); |
| 142 | + var node = rootNode; |
| 143 | + var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex; |
| 144 | + |
| 145 | + while (i--) { |
| 146 | + nodeIndex = parseInt(nodeIndices[i], 10); |
| 147 | + if (nodeIndex < node.childNodes.length) { |
| 148 | + node = node.childNodes[parseInt(nodeIndices[i], 10)]; |
| 149 | + } else { |
| 150 | + throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) + |
| 151 | + " has no child with index " + nodeIndex + ", " + i); |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + return new dom.DomPosition(node, parseInt(bits[1], 10)); |
| 156 | + } |
| 157 | + |
| 158 | + function serializeRange(range, omitChecksum, rootNode) { |
| 159 | + rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement; |
| 160 | + if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) { |
| 161 | + throw new Error("serializeRange: range is not wholly contained within specified root node"); |
| 162 | + } |
| 163 | + var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," + |
| 164 | + serializePosition(range.endContainer, range.endOffset, rootNode); |
| 165 | + if (!omitChecksum) { |
| 166 | + serialized += "{" + getElementChecksum(rootNode) + "}"; |
| 167 | + } |
| 168 | + return serialized; |
| 169 | + } |
| 170 | + |
| 171 | + function deserializeRange(serialized, rootNode, doc) { |
| 172 | + if (rootNode) { |
| 173 | + doc = doc || dom.getDocument(rootNode); |
| 174 | + } else { |
| 175 | + doc = doc || document; |
| 176 | + rootNode = doc.documentElement; |
| 177 | + } |
| 178 | + var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized); |
| 179 | + var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode); |
| 180 | + if (checksum && checksum !== getElementChecksum(rootNode)) { |
| 181 | + throw new Error("deserializeRange: checksums of serialized range root node (" + checksum + |
| 182 | + ") and target root node (" + rootNodeChecksum + ") do not match"); |
| 183 | + } |
| 184 | + var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc); |
| 185 | + var range = api.createRange(doc); |
| 186 | + range.setStart(start.node, start.offset); |
| 187 | + range.setEnd(end.node, end.offset); |
| 188 | + return range; |
| 189 | + } |
| 190 | + |
| 191 | + function canDeserializeRange(serialized, rootNode, doc) { |
| 192 | + if (rootNode) { |
| 193 | + doc = doc || dom.getDocument(rootNode); |
| 194 | + } else { |
| 195 | + doc = doc || document; |
| 196 | + rootNode = doc.documentElement; |
| 197 | + } |
| 198 | + var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized); |
| 199 | + var checksum = result[3]; |
| 200 | + return !checksum || checksum === getElementChecksum(rootNode); |
| 201 | + } |
| 202 | + |
| 203 | + function serializeSelection(selection, omitChecksum, rootNode) { |
| 204 | + selection = selection || api.getSelection(); |
| 205 | + var ranges = selection.getAllRanges(), serializedRanges = []; |
| 206 | + for (var i = 0, len = ranges.length; i < len; ++i) { |
| 207 | + serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode); |
| 208 | + } |
| 209 | + return serializedRanges.join("|"); |
| 210 | + } |
| 211 | + |
| 212 | + function deserializeSelection(serialized, rootNode, win) { |
| 213 | + if (rootNode) { |
| 214 | + win = win || dom.getWindow(rootNode); |
| 215 | + } else { |
| 216 | + win = win || window; |
| 217 | + rootNode = win.document.documentElement; |
| 218 | + } |
| 219 | + var serializedRanges = serialized.split("|"); |
| 220 | + var sel = api.getSelection(win); |
| 221 | + var ranges = []; |
| 222 | + |
| 223 | + for (var i = 0, len = serializedRanges.length; i < len; ++i) { |
| 224 | + ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document); |
| 225 | + } |
| 226 | + sel.setRanges(ranges); |
| 227 | + |
| 228 | + return sel; |
| 229 | + } |
| 230 | + |
| 231 | + function canDeserializeSelection(serialized, rootNode, win) { |
| 232 | + var doc; |
| 233 | + if (rootNode) { |
| 234 | + doc = win ? win.document : dom.getDocument(rootNode); |
| 235 | + } else { |
| 236 | + win = win || window; |
| 237 | + rootNode = win.document.documentElement; |
| 238 | + } |
| 239 | + var serializedRanges = serialized.split("|"); |
| 240 | + |
| 241 | + for (var i = 0, len = serializedRanges.length; i < len; ++i) { |
| 242 | + if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) { |
| 243 | + return false; |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + return true; |
| 248 | + } |
| 249 | + |
| 250 | + |
| 251 | + var cookieName = "rangySerializedSelection"; |
| 252 | + |
| 253 | + function getSerializedSelectionFromCookie(cookie) { |
| 254 | + var parts = cookie.split(/[;,]/); |
| 255 | + for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) { |
| 256 | + nameVal = parts[i].split("="); |
| 257 | + if (nameVal[0].replace(/^\s+/, "") == cookieName) { |
| 258 | + val = nameVal[1]; |
| 259 | + if (val) { |
| 260 | + return decodeURIComponent(val.replace(/\s+$/, "")); |
| 261 | + } |
| 262 | + } |
| 263 | + } |
| 264 | + return null; |
| 265 | + } |
| 266 | + |
| 267 | + function restoreSelectionFromCookie(win) { |
| 268 | + win = win || window; |
| 269 | + var serialized = getSerializedSelectionFromCookie(win.document.cookie); |
| 270 | + if (serialized) { |
| 271 | + deserializeSelection(serialized, win.doc) |
| 272 | + } |
| 273 | + } |
| 274 | + |
| 275 | + function saveSelectionCookie(win, props) { |
| 276 | + win = win || window; |
| 277 | + props = (typeof props == "object") ? props : {}; |
| 278 | + var expires = props.expires ? ";expires=" + props.expires.toUTCString() : ""; |
| 279 | + var path = props.path ? ";path=" + props.path : ""; |
| 280 | + var domain = props.domain ? ";domain=" + props.domain : ""; |
| 281 | + var secure = props.secure ? ";secure" : ""; |
| 282 | + var serialized = serializeSelection(api.getSelection(win)); |
| 283 | + win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure; |
| 284 | + } |
| 285 | + |
| 286 | + api.serializePosition = serializePosition; |
| 287 | + api.deserializePosition = deserializePosition; |
| 288 | + |
| 289 | + api.serializeRange = serializeRange; |
| 290 | + api.deserializeRange = deserializeRange; |
| 291 | + api.canDeserializeRange = canDeserializeRange; |
| 292 | + |
| 293 | + api.serializeSelection = serializeSelection; |
| 294 | + api.deserializeSelection = deserializeSelection; |
| 295 | + api.canDeserializeSelection = canDeserializeSelection; |
| 296 | + |
| 297 | + api.restoreSelectionFromCookie = restoreSelectionFromCookie; |
| 298 | + api.saveSelectionCookie = saveSelectionCookie; |
| 299 | + |
| 300 | + api.getElementChecksum = getElementChecksum; |
| 301 | +}); |
Property changes on: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-serializer.js |
___________________________________________________________________ |
Added: svn:eol-style |
302 | 302 | + native |
Index: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-cssclassapplier.js |
— | — | @@ -1,713 +1,713 @@ |
2 | | -/**
|
3 | | - * @license CSS Class Applier module for Rangy.
|
4 | | - * Adds, removes and toggles CSS classes on Ranges and Selections
|
5 | | - *
|
6 | | - * Part of Rangy, a cross-browser JavaScript range and selection library
|
7 | | - * http://code.google.com/p/rangy/
|
8 | | - *
|
9 | | - * Depends on Rangy core.
|
10 | | - *
|
11 | | - * Copyright 2011, Tim Down
|
12 | | - * Licensed under the MIT license.
|
13 | | - * Version: 1.2.2
|
14 | | - * Build date: 13 November 2011
|
15 | | - */
|
16 | | -rangy.createModule("CssClassApplier", function(api, module) {
|
17 | | - api.requireModules( ["WrappedSelection", "WrappedRange"] );
|
18 | | -
|
19 | | - var dom = api.dom;
|
20 | | -
|
21 | | -
|
22 | | -
|
23 | | - var defaultTagName = "span";
|
24 | | -
|
25 | | - function trim(str) {
|
26 | | - return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
|
27 | | - }
|
28 | | -
|
29 | | - function hasClass(el, cssClass) {
|
30 | | - return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className);
|
31 | | - }
|
32 | | -
|
33 | | - function addClass(el, cssClass) {
|
34 | | - if (el.className) {
|
35 | | - if (!hasClass(el, cssClass)) {
|
36 | | - el.className += " " + cssClass;
|
37 | | - }
|
38 | | - } else {
|
39 | | - el.className = cssClass;
|
40 | | - }
|
41 | | - }
|
42 | | -
|
43 | | - var removeClass = (function() {
|
44 | | - function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
|
45 | | - return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
|
46 | | - }
|
47 | | -
|
48 | | - return function(el, cssClass) {
|
49 | | - if (el.className) {
|
50 | | - el.className = el.className.replace(new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)"), replacer);
|
51 | | - }
|
52 | | - };
|
53 | | - })();
|
54 | | -
|
55 | | - function sortClassName(className) {
|
56 | | - return className.split(/\s+/).sort().join(" ");
|
57 | | - }
|
58 | | -
|
59 | | - function getSortedClassName(el) {
|
60 | | - return sortClassName(el.className);
|
61 | | - }
|
62 | | -
|
63 | | - function haveSameClasses(el1, el2) {
|
64 | | - return getSortedClassName(el1) == getSortedClassName(el2);
|
65 | | - }
|
66 | | -
|
67 | | - function replaceWithOwnChildren(el) {
|
68 | | -
|
69 | | - var parent = el.parentNode;
|
70 | | - while (el.hasChildNodes()) {
|
71 | | - parent.insertBefore(el.firstChild, el);
|
72 | | - }
|
73 | | - parent.removeChild(el);
|
74 | | - }
|
75 | | -
|
76 | | - function rangeSelectsAnyText(range, textNode) {
|
77 | | - var textRange = range.cloneRange();
|
78 | | - textRange.selectNodeContents(textNode);
|
79 | | -
|
80 | | - var intersectionRange = textRange.intersection(range);
|
81 | | - var text = intersectionRange ? intersectionRange.toString() : "";
|
82 | | - textRange.detach();
|
83 | | -
|
84 | | - return text != "";
|
85 | | - }
|
86 | | -
|
87 | | - function getEffectiveTextNodes(range) {
|
88 | | - return range.getNodes([3], function(textNode) {
|
89 | | - return rangeSelectsAnyText(range, textNode);
|
90 | | - });
|
91 | | - }
|
92 | | -
|
93 | | - function elementsHaveSameNonClassAttributes(el1, el2) {
|
94 | | - if (el1.attributes.length != el2.attributes.length) return false;
|
95 | | - for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
|
96 | | - attr1 = el1.attributes[i];
|
97 | | - name = attr1.name;
|
98 | | - if (name != "class") {
|
99 | | - attr2 = el2.attributes.getNamedItem(name);
|
100 | | - if (attr1.specified != attr2.specified) return false;
|
101 | | - if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
|
102 | | - }
|
103 | | - }
|
104 | | - return true;
|
105 | | - }
|
106 | | -
|
107 | | - function elementHasNonClassAttributes(el, exceptions) {
|
108 | | - for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
|
109 | | - attrName = el.attributes[i].name;
|
110 | | - if ( !(exceptions && dom.arrayContains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
|
111 | | - return true;
|
112 | | - }
|
113 | | - }
|
114 | | - return false;
|
115 | | - }
|
116 | | -
|
117 | | - function elementHasProps(el, props) {
|
118 | | - for (var p in props) {
|
119 | | - if (props.hasOwnProperty(p) && el[p] !== props[p]) {
|
120 | | - return false;
|
121 | | - }
|
122 | | - }
|
123 | | - return true;
|
124 | | - }
|
125 | | -
|
126 | | - var getComputedStyleProperty;
|
127 | | -
|
128 | | - if (typeof window.getComputedStyle != "undefined") {
|
129 | | - getComputedStyleProperty = function(el, propName) {
|
130 | | - return dom.getWindow(el).getComputedStyle(el, null)[propName];
|
131 | | - };
|
132 | | - } else if (typeof document.documentElement.currentStyle != "undefined") {
|
133 | | - getComputedStyleProperty = function(el, propName) {
|
134 | | - return el.currentStyle[propName];
|
135 | | - };
|
136 | | - } else {
|
137 | | - module.fail("No means of obtaining computed style properties found");
|
138 | | - }
|
139 | | -
|
140 | | - var isEditableElement;
|
141 | | -
|
142 | | - (function() {
|
143 | | - var testEl = document.createElement("div");
|
144 | | - if (typeof testEl.isContentEditable == "boolean") {
|
145 | | - isEditableElement = function(node) {
|
146 | | - return node && node.nodeType == 1 && node.isContentEditable;
|
147 | | - };
|
148 | | - } else {
|
149 | | - isEditableElement = function(node) {
|
150 | | - if (!node || node.nodeType != 1 || node.contentEditable == "false") {
|
151 | | - return false;
|
152 | | - }
|
153 | | - return node.contentEditable == "true" || isEditableElement(node.parentNode);
|
154 | | - };
|
155 | | - }
|
156 | | - })();
|
157 | | -
|
158 | | - function isEditingHost(node) {
|
159 | | - var parent;
|
160 | | - return node && node.nodeType == 1
|
161 | | - && (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on")
|
162 | | - || (isEditableElement(node) && !isEditableElement(node.parentNode)));
|
163 | | - }
|
164 | | -
|
165 | | - function isEditable(node) {
|
166 | | - return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
|
167 | | - }
|
168 | | -
|
169 | | - var inlineDisplayRegex = /^inline(-block|-table)?$/i;
|
170 | | -
|
171 | | - function isNonInlineElement(node) {
|
172 | | - return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
|
173 | | - }
|
174 | | -
|
175 | | - // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
|
176 | | - var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
|
177 | | -
|
178 | | - function isUnrenderedWhiteSpaceNode(node) {
|
179 | | - if (node.data.length == 0) {
|
180 | | - return true;
|
181 | | - }
|
182 | | - if (htmlNonWhiteSpaceRegex.test(node.data)) {
|
183 | | - return false;
|
184 | | - }
|
185 | | - var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
|
186 | | - switch (cssWhiteSpace) {
|
187 | | - case "pre":
|
188 | | - case "pre-wrap":
|
189 | | - case "-moz-pre-wrap":
|
190 | | - return false;
|
191 | | - case "pre-line":
|
192 | | - if (/[\r\n]/.test(node.data)) {
|
193 | | - return false;
|
194 | | - }
|
195 | | - }
|
196 | | -
|
197 | | - // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
|
198 | | - // non-inline element, it will not be rendered. This seems to be a good enough definition.
|
199 | | - return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
|
200 | | - }
|
201 | | -
|
202 | | - function isSplitPoint(node, offset) {
|
203 | | - if (dom.isCharacterDataNode(node)) {
|
204 | | - if (offset == 0) {
|
205 | | - return !!node.previousSibling;
|
206 | | - } else if (offset == node.length) {
|
207 | | - return !!node.nextSibling;
|
208 | | - } else {
|
209 | | - return true;
|
210 | | - }
|
211 | | - }
|
212 | | -
|
213 | | - return offset > 0 && offset < node.childNodes.length;
|
214 | | - }
|
215 | | -
|
216 | | - function splitNodeAt(node, descendantNode, descendantOffset, rangesToPreserve) {
|
217 | | - var newNode;
|
218 | | - var splitAtStart = (descendantOffset == 0);
|
219 | | -
|
220 | | - if (dom.isAncestorOf(descendantNode, node)) {
|
221 | | -
|
222 | | - return node;
|
223 | | - }
|
224 | | -
|
225 | | - if (dom.isCharacterDataNode(descendantNode)) {
|
226 | | - if (descendantOffset == 0) {
|
227 | | - descendantOffset = dom.getNodeIndex(descendantNode);
|
228 | | - descendantNode = descendantNode.parentNode;
|
229 | | - } else if (descendantOffset == descendantNode.length) {
|
230 | | - descendantOffset = dom.getNodeIndex(descendantNode) + 1;
|
231 | | - descendantNode = descendantNode.parentNode;
|
232 | | - } else {
|
233 | | - throw module.createError("splitNodeAt should not be called with offset in the middle of a data node ("
|
234 | | - + descendantOffset + " in " + descendantNode.data);
|
235 | | - }
|
236 | | - }
|
237 | | -
|
238 | | - if (isSplitPoint(descendantNode, descendantOffset)) {
|
239 | | - if (!newNode) {
|
240 | | - newNode = descendantNode.cloneNode(false);
|
241 | | - if (newNode.id) {
|
242 | | - newNode.removeAttribute("id");
|
243 | | - }
|
244 | | - var child;
|
245 | | - while ((child = descendantNode.childNodes[descendantOffset])) {
|
246 | | - newNode.appendChild(child);
|
247 | | - }
|
248 | | - dom.insertAfter(newNode, descendantNode);
|
249 | | - }
|
250 | | - return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve);
|
251 | | - } else if (node != descendantNode) {
|
252 | | - newNode = descendantNode.parentNode;
|
253 | | -
|
254 | | - // Work out a new split point in the parent node
|
255 | | - var newNodeIndex = dom.getNodeIndex(descendantNode);
|
256 | | -
|
257 | | - if (!splitAtStart) {
|
258 | | - newNodeIndex++;
|
259 | | - }
|
260 | | - return splitNodeAt(node, newNode, newNodeIndex, rangesToPreserve);
|
261 | | - }
|
262 | | - return node;
|
263 | | - }
|
264 | | -
|
265 | | - function areElementsMergeable(el1, el2) {
|
266 | | - return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2);
|
267 | | - }
|
268 | | -
|
269 | | - function createAdjacentMergeableTextNodeGetter(forward) {
|
270 | | - var propName = forward ? "nextSibling" : "previousSibling";
|
271 | | -
|
272 | | - return function(textNode, checkParentElement) {
|
273 | | - var el = textNode.parentNode;
|
274 | | - var adjacentNode = textNode[propName];
|
275 | | - if (adjacentNode) {
|
276 | | - // Can merge if the node's previous/next sibling is a text node
|
277 | | - if (adjacentNode && adjacentNode.nodeType == 3) {
|
278 | | - return adjacentNode;
|
279 | | - }
|
280 | | - } else if (checkParentElement) {
|
281 | | - // Compare text node parent element with its sibling
|
282 | | - adjacentNode = el[propName];
|
283 | | -
|
284 | | - if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
|
285 | | - return adjacentNode[forward ? "firstChild" : "lastChild"];
|
286 | | - }
|
287 | | - }
|
288 | | - return null;
|
289 | | - }
|
290 | | - }
|
291 | | -
|
292 | | - var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
|
293 | | - getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
|
294 | | -
|
295 | | -
|
296 | | - function Merge(firstNode) {
|
297 | | - this.isElementMerge = (firstNode.nodeType == 1);
|
298 | | - this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
|
299 | | - this.textNodes = [this.firstTextNode];
|
300 | | - }
|
301 | | -
|
302 | | - Merge.prototype = {
|
303 | | - doMerge: function() {
|
304 | | - var textBits = [], textNode, parent, text;
|
305 | | - for (var i = 0, len = this.textNodes.length; i < len; ++i) {
|
306 | | - textNode = this.textNodes[i];
|
307 | | - parent = textNode.parentNode;
|
308 | | - textBits[i] = textNode.data;
|
309 | | - if (i) {
|
310 | | - parent.removeChild(textNode);
|
311 | | - if (!parent.hasChildNodes()) {
|
312 | | - parent.parentNode.removeChild(parent);
|
313 | | - }
|
314 | | - }
|
315 | | - }
|
316 | | - this.firstTextNode.data = text = textBits.join("");
|
317 | | - return text;
|
318 | | - },
|
319 | | -
|
320 | | - getLength: function() {
|
321 | | - var i = this.textNodes.length, len = 0;
|
322 | | - while (i--) {
|
323 | | - len += this.textNodes[i].length;
|
324 | | - }
|
325 | | - return len;
|
326 | | - },
|
327 | | -
|
328 | | - toString: function() {
|
329 | | - var textBits = [];
|
330 | | - for (var i = 0, len = this.textNodes.length; i < len; ++i) {
|
331 | | - textBits[i] = "'" + this.textNodes[i].data + "'";
|
332 | | - }
|
333 | | - return "[Merge(" + textBits.join(",") + ")]";
|
334 | | - }
|
335 | | - };
|
336 | | -
|
337 | | - var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"];
|
338 | | -
|
339 | | - // Allow "class" as a property name in object properties
|
340 | | - var mappedPropertyNames = {"class" : "className"};
|
341 | | -
|
342 | | - function CssClassApplier(cssClass, options, tagNames) {
|
343 | | - this.cssClass = cssClass;
|
344 | | - var normalize, i, len, propName;
|
345 | | -
|
346 | | - var elementPropertiesFromOptions = null;
|
347 | | -
|
348 | | - // Initialize from options object
|
349 | | - if (typeof options == "object" && options !== null) {
|
350 | | - tagNames = options.tagNames;
|
351 | | - elementPropertiesFromOptions = options.elementProperties;
|
352 | | -
|
353 | | - for (i = 0; propName = optionProperties[i++]; ) {
|
354 | | - if (options.hasOwnProperty(propName)) {
|
355 | | - this[propName] = options[propName];
|
356 | | - }
|
357 | | - }
|
358 | | - normalize = options.normalize;
|
359 | | - } else {
|
360 | | - normalize = options;
|
361 | | - }
|
362 | | -
|
363 | | - // Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization
|
364 | | - this.normalize = (typeof normalize == "undefined") ? true : normalize;
|
365 | | -
|
366 | | - // Initialize element properties and attribute exceptions
|
367 | | - this.attrExceptions = [];
|
368 | | - var el = document.createElement(this.elementTagName);
|
369 | | - this.elementProperties = {};
|
370 | | - for (var p in elementPropertiesFromOptions) {
|
371 | | - if (elementPropertiesFromOptions.hasOwnProperty(p)) {
|
372 | | - // Map "class" to "className"
|
373 | | - if (mappedPropertyNames.hasOwnProperty(p)) {
|
374 | | - p = mappedPropertyNames[p];
|
375 | | - }
|
376 | | - el[p] = elementPropertiesFromOptions[p];
|
377 | | -
|
378 | | - // Copy the property back from the dummy element so that later comparisons to check whether elements
|
379 | | - // may be removed are checking against the right value. For example, the href property of an element
|
380 | | - // returns a fully qualified URL even if it was previously assigned a relative URL.
|
381 | | - this.elementProperties[p] = el[p];
|
382 | | - this.attrExceptions.push(p);
|
383 | | - }
|
384 | | - }
|
385 | | -
|
386 | | - this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ?
|
387 | | - sortClassName(this.elementProperties.className + " " + cssClass) : cssClass;
|
388 | | -
|
389 | | - // Initialize tag names
|
390 | | - this.applyToAnyTagName = false;
|
391 | | - var type = typeof tagNames;
|
392 | | - if (type == "string") {
|
393 | | - if (tagNames == "*") {
|
394 | | - this.applyToAnyTagName = true;
|
395 | | - } else {
|
396 | | - this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
|
397 | | - }
|
398 | | - } else if (type == "object" && typeof tagNames.length == "number") {
|
399 | | - this.tagNames = [];
|
400 | | - for (i = 0, len = tagNames.length; i < len; ++i) {
|
401 | | - if (tagNames[i] == "*") {
|
402 | | - this.applyToAnyTagName = true;
|
403 | | - } else {
|
404 | | - this.tagNames.push(tagNames[i].toLowerCase());
|
405 | | - }
|
406 | | - }
|
407 | | - } else {
|
408 | | - this.tagNames = [this.elementTagName];
|
409 | | - }
|
410 | | - }
|
411 | | -
|
412 | | - CssClassApplier.prototype = {
|
413 | | - elementTagName: defaultTagName,
|
414 | | - elementProperties: {},
|
415 | | - ignoreWhiteSpace: true,
|
416 | | - applyToEditableOnly: false,
|
417 | | -
|
418 | | - hasClass: function(node) {
|
419 | | - return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass);
|
420 | | - },
|
421 | | -
|
422 | | - getSelfOrAncestorWithClass: function(node) {
|
423 | | - while (node) {
|
424 | | - if (this.hasClass(node, this.cssClass)) {
|
425 | | - return node;
|
426 | | - }
|
427 | | - node = node.parentNode;
|
428 | | - }
|
429 | | - return null;
|
430 | | - },
|
431 | | -
|
432 | | - isModifiable: function(node) {
|
433 | | - return !this.applyToEditableOnly || isEditable(node);
|
434 | | - },
|
435 | | -
|
436 | | - // White space adjacent to an unwrappable node can be ignored for wrapping
|
437 | | - isIgnorableWhiteSpaceNode: function(node) {
|
438 | | - return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
|
439 | | - },
|
440 | | -
|
441 | | - // Normalizes nodes after applying a CSS class to a Range.
|
442 | | - postApply: function(textNodes, range, isUndo) {
|
443 | | -
|
444 | | - var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
|
445 | | -
|
446 | | - var merges = [], currentMerge;
|
447 | | -
|
448 | | - var rangeStartNode = firstNode, rangeEndNode = lastNode;
|
449 | | - var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
|
450 | | -
|
451 | | - var textNode, precedingTextNode;
|
452 | | -
|
453 | | - for (var i = 0, len = textNodes.length; i < len; ++i) {
|
454 | | - textNode = textNodes[i];
|
455 | | - precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
|
456 | | -
|
457 | | - if (precedingTextNode) {
|
458 | | - if (!currentMerge) {
|
459 | | - currentMerge = new Merge(precedingTextNode);
|
460 | | - merges.push(currentMerge);
|
461 | | - }
|
462 | | - currentMerge.textNodes.push(textNode);
|
463 | | - if (textNode === firstNode) {
|
464 | | - rangeStartNode = currentMerge.firstTextNode;
|
465 | | - rangeStartOffset = rangeStartNode.length;
|
466 | | - }
|
467 | | - if (textNode === lastNode) {
|
468 | | - rangeEndNode = currentMerge.firstTextNode;
|
469 | | - rangeEndOffset = currentMerge.getLength();
|
470 | | - }
|
471 | | - } else {
|
472 | | - currentMerge = null;
|
473 | | - }
|
474 | | - }
|
475 | | -
|
476 | | - // Test whether the first node after the range needs merging
|
477 | | - var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
|
478 | | -
|
479 | | - if (nextTextNode) {
|
480 | | - if (!currentMerge) {
|
481 | | - currentMerge = new Merge(lastNode);
|
482 | | - merges.push(currentMerge);
|
483 | | - }
|
484 | | - currentMerge.textNodes.push(nextTextNode);
|
485 | | - }
|
486 | | -
|
487 | | - // Do the merges
|
488 | | - if (merges.length) {
|
489 | | -
|
490 | | - for (i = 0, len = merges.length; i < len; ++i) {
|
491 | | - merges[i].doMerge();
|
492 | | - }
|
493 | | -
|
494 | | -
|
495 | | - // Set the range boundaries
|
496 | | - range.setStart(rangeStartNode, rangeStartOffset);
|
497 | | - range.setEnd(rangeEndNode, rangeEndOffset);
|
498 | | - }
|
499 | | -
|
500 | | - },
|
501 | | -
|
502 | | - createContainer: function(doc) {
|
503 | | - var el = doc.createElement(this.elementTagName);
|
504 | | - api.util.extend(el, this.elementProperties);
|
505 | | - addClass(el, this.cssClass);
|
506 | | - return el;
|
507 | | - },
|
508 | | -
|
509 | | - applyToTextNode: function(textNode) {
|
510 | | -
|
511 | | -
|
512 | | - var parent = textNode.parentNode;
|
513 | | - if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
|
514 | | - addClass(parent, this.cssClass);
|
515 | | - } else {
|
516 | | - var el = this.createContainer(dom.getDocument(textNode));
|
517 | | - textNode.parentNode.insertBefore(el, textNode);
|
518 | | - el.appendChild(textNode);
|
519 | | - }
|
520 | | -
|
521 | | - },
|
522 | | -
|
523 | | - isRemovable: function(el) {
|
524 | | - return el.tagName.toLowerCase() == this.elementTagName
|
525 | | - && getSortedClassName(el) == this.elementSortedClassName
|
526 | | - && elementHasProps(el, this.elementProperties)
|
527 | | - && !elementHasNonClassAttributes(el, this.attrExceptions)
|
528 | | - && this.isModifiable(el);
|
529 | | - },
|
530 | | -
|
531 | | - undoToTextNode: function(textNode, range, ancestorWithClass) {
|
532 | | -
|
533 | | - if (!range.containsNode(ancestorWithClass)) {
|
534 | | - // Split out the portion of the ancestor from which we can remove the CSS class
|
535 | | - //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
|
536 | | - var ancestorRange = range.cloneRange();
|
537 | | - ancestorRange.selectNode(ancestorWithClass);
|
538 | | -
|
539 | | - if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) {
|
540 | | - splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]);
|
541 | | - range.setEndAfter(ancestorWithClass);
|
542 | | - }
|
543 | | - if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) {
|
544 | | - ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]);
|
545 | | - }
|
546 | | - }
|
547 | | -
|
548 | | - if (this.isRemovable(ancestorWithClass)) {
|
549 | | - replaceWithOwnChildren(ancestorWithClass);
|
550 | | - } else {
|
551 | | - removeClass(ancestorWithClass, this.cssClass);
|
552 | | - }
|
553 | | - },
|
554 | | -
|
555 | | - applyToRange: function(range) {
|
556 | | - range.splitBoundaries();
|
557 | | - var textNodes = getEffectiveTextNodes(range);
|
558 | | -
|
559 | | - if (textNodes.length) {
|
560 | | - var textNode;
|
561 | | -
|
562 | | - for (var i = 0, len = textNodes.length; i < len; ++i) {
|
563 | | - textNode = textNodes[i];
|
564 | | -
|
565 | | - if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode)
|
566 | | - && this.isModifiable(textNode)) {
|
567 | | - this.applyToTextNode(textNode);
|
568 | | - }
|
569 | | - }
|
570 | | - range.setStart(textNodes[0], 0);
|
571 | | - textNode = textNodes[textNodes.length - 1];
|
572 | | - range.setEnd(textNode, textNode.length);
|
573 | | - if (this.normalize) {
|
574 | | - this.postApply(textNodes, range, false);
|
575 | | - }
|
576 | | - }
|
577 | | - },
|
578 | | -
|
579 | | - applyToSelection: function(win) {
|
580 | | -
|
581 | | - win = win || window;
|
582 | | - var sel = api.getSelection(win);
|
583 | | -
|
584 | | - var range, ranges = sel.getAllRanges();
|
585 | | - sel.removeAllRanges();
|
586 | | - var i = ranges.length;
|
587 | | - while (i--) {
|
588 | | - range = ranges[i];
|
589 | | - this.applyToRange(range);
|
590 | | - sel.addRange(range);
|
591 | | - }
|
592 | | -
|
593 | | - },
|
594 | | -
|
595 | | - undoToRange: function(range) {
|
596 | | -
|
597 | | - range.splitBoundaries();
|
598 | | - var textNodes = getEffectiveTextNodes(range);
|
599 | | - var textNode, ancestorWithClass;
|
600 | | - var lastTextNode = textNodes[textNodes.length - 1];
|
601 | | -
|
602 | | - if (textNodes.length) {
|
603 | | - for (var i = 0, len = textNodes.length; i < len; ++i) {
|
604 | | - textNode = textNodes[i];
|
605 | | - ancestorWithClass = this.getSelfOrAncestorWithClass(textNode);
|
606 | | - if (ancestorWithClass && this.isModifiable(textNode)) {
|
607 | | - this.undoToTextNode(textNode, range, ancestorWithClass);
|
608 | | - }
|
609 | | -
|
610 | | - // Ensure the range is still valid
|
611 | | - range.setStart(textNodes[0], 0);
|
612 | | - range.setEnd(lastTextNode, lastTextNode.length);
|
613 | | - }
|
614 | | -
|
615 | | -
|
616 | | -
|
617 | | - if (this.normalize) {
|
618 | | - this.postApply(textNodes, range, true);
|
619 | | - }
|
620 | | - }
|
621 | | - },
|
622 | | -
|
623 | | - undoToSelection: function(win) {
|
624 | | - win = win || window;
|
625 | | - var sel = api.getSelection(win);
|
626 | | - var ranges = sel.getAllRanges(), range;
|
627 | | - sel.removeAllRanges();
|
628 | | - for (var i = 0, len = ranges.length; i < len; ++i) {
|
629 | | - range = ranges[i];
|
630 | | - this.undoToRange(range);
|
631 | | - sel.addRange(range);
|
632 | | - }
|
633 | | - },
|
634 | | -
|
635 | | - getTextSelectedByRange: function(textNode, range) {
|
636 | | - var textRange = range.cloneRange();
|
637 | | - textRange.selectNodeContents(textNode);
|
638 | | -
|
639 | | - var intersectionRange = textRange.intersection(range);
|
640 | | - var text = intersectionRange ? intersectionRange.toString() : "";
|
641 | | - textRange.detach();
|
642 | | -
|
643 | | - return text;
|
644 | | - },
|
645 | | -
|
646 | | - isAppliedToRange: function(range) {
|
647 | | - if (range.collapsed) {
|
648 | | - return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
|
649 | | - } else {
|
650 | | - var textNodes = range.getNodes( [3] );
|
651 | | - for (var i = 0, textNode; textNode = textNodes[i++]; ) {
|
652 | | - if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode)
|
653 | | - && this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
|
654 | | - return false;
|
655 | | - }
|
656 | | - }
|
657 | | - return true;
|
658 | | - }
|
659 | | - },
|
660 | | -
|
661 | | - isAppliedToSelection: function(win) {
|
662 | | - win = win || window;
|
663 | | - var sel = api.getSelection(win);
|
664 | | - var ranges = sel.getAllRanges();
|
665 | | - var i = ranges.length;
|
666 | | - while (i--) {
|
667 | | - if (!this.isAppliedToRange(ranges[i])) {
|
668 | | - return false;
|
669 | | - }
|
670 | | - }
|
671 | | -
|
672 | | - return true;
|
673 | | - },
|
674 | | -
|
675 | | - toggleRange: function(range) {
|
676 | | - if (this.isAppliedToRange(range)) {
|
677 | | - this.undoToRange(range);
|
678 | | - } else {
|
679 | | - this.applyToRange(range);
|
680 | | - }
|
681 | | - },
|
682 | | -
|
683 | | - toggleSelection: function(win) {
|
684 | | - if (this.isAppliedToSelection(win)) {
|
685 | | - this.undoToSelection(win);
|
686 | | - } else {
|
687 | | - this.applyToSelection(win);
|
688 | | - }
|
689 | | - },
|
690 | | -
|
691 | | - detach: function() {}
|
692 | | - };
|
693 | | -
|
694 | | - function createCssClassApplier(cssClass, options, tagNames) {
|
695 | | - return new CssClassApplier(cssClass, options, tagNames);
|
696 | | - }
|
697 | | -
|
698 | | - CssClassApplier.util = {
|
699 | | - hasClass: hasClass,
|
700 | | - addClass: addClass,
|
701 | | - removeClass: removeClass,
|
702 | | - hasSameClasses: haveSameClasses,
|
703 | | - replaceWithOwnChildren: replaceWithOwnChildren,
|
704 | | - elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
|
705 | | - elementHasNonClassAttributes: elementHasNonClassAttributes,
|
706 | | - splitNodeAt: splitNodeAt,
|
707 | | - isEditableElement: isEditableElement,
|
708 | | - isEditingHost: isEditingHost,
|
709 | | - isEditable: isEditable
|
710 | | - };
|
711 | | -
|
712 | | - api.CssClassApplier = CssClassApplier;
|
713 | | - api.createCssClassApplier = createCssClassApplier;
|
714 | | -});
|
| 2 | +/** |
| 3 | + * @license CSS Class Applier module for Rangy. |
| 4 | + * Adds, removes and toggles CSS classes on Ranges and Selections |
| 5 | + * |
| 6 | + * Part of Rangy, a cross-browser JavaScript range and selection library |
| 7 | + * http://code.google.com/p/rangy/ |
| 8 | + * |
| 9 | + * Depends on Rangy core. |
| 10 | + * |
| 11 | + * Copyright 2011, Tim Down |
| 12 | + * Licensed under the MIT license. |
| 13 | + * Version: 1.2.2 |
| 14 | + * Build date: 13 November 2011 |
| 15 | + */ |
| 16 | +rangy.createModule("CssClassApplier", function(api, module) { |
| 17 | + api.requireModules( ["WrappedSelection", "WrappedRange"] ); |
| 18 | + |
| 19 | + var dom = api.dom; |
| 20 | + |
| 21 | + |
| 22 | + |
| 23 | + var defaultTagName = "span"; |
| 24 | + |
| 25 | + function trim(str) { |
| 26 | + return str.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); |
| 27 | + } |
| 28 | + |
| 29 | + function hasClass(el, cssClass) { |
| 30 | + return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className); |
| 31 | + } |
| 32 | + |
| 33 | + function addClass(el, cssClass) { |
| 34 | + if (el.className) { |
| 35 | + if (!hasClass(el, cssClass)) { |
| 36 | + el.className += " " + cssClass; |
| 37 | + } |
| 38 | + } else { |
| 39 | + el.className = cssClass; |
| 40 | + } |
| 41 | + } |
| 42 | + |
| 43 | + var removeClass = (function() { |
| 44 | + function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) { |
| 45 | + return (whiteSpaceBefore && whiteSpaceAfter) ? " " : ""; |
| 46 | + } |
| 47 | + |
| 48 | + return function(el, cssClass) { |
| 49 | + if (el.className) { |
| 50 | + el.className = el.className.replace(new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)"), replacer); |
| 51 | + } |
| 52 | + }; |
| 53 | + })(); |
| 54 | + |
| 55 | + function sortClassName(className) { |
| 56 | + return className.split(/\s+/).sort().join(" "); |
| 57 | + } |
| 58 | + |
| 59 | + function getSortedClassName(el) { |
| 60 | + return sortClassName(el.className); |
| 61 | + } |
| 62 | + |
| 63 | + function haveSameClasses(el1, el2) { |
| 64 | + return getSortedClassName(el1) == getSortedClassName(el2); |
| 65 | + } |
| 66 | + |
| 67 | + function replaceWithOwnChildren(el) { |
| 68 | + |
| 69 | + var parent = el.parentNode; |
| 70 | + while (el.hasChildNodes()) { |
| 71 | + parent.insertBefore(el.firstChild, el); |
| 72 | + } |
| 73 | + parent.removeChild(el); |
| 74 | + } |
| 75 | + |
| 76 | + function rangeSelectsAnyText(range, textNode) { |
| 77 | + var textRange = range.cloneRange(); |
| 78 | + textRange.selectNodeContents(textNode); |
| 79 | + |
| 80 | + var intersectionRange = textRange.intersection(range); |
| 81 | + var text = intersectionRange ? intersectionRange.toString() : ""; |
| 82 | + textRange.detach(); |
| 83 | + |
| 84 | + return text != ""; |
| 85 | + } |
| 86 | + |
| 87 | + function getEffectiveTextNodes(range) { |
| 88 | + return range.getNodes([3], function(textNode) { |
| 89 | + return rangeSelectsAnyText(range, textNode); |
| 90 | + }); |
| 91 | + } |
| 92 | + |
| 93 | + function elementsHaveSameNonClassAttributes(el1, el2) { |
| 94 | + if (el1.attributes.length != el2.attributes.length) return false; |
| 95 | + for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { |
| 96 | + attr1 = el1.attributes[i]; |
| 97 | + name = attr1.name; |
| 98 | + if (name != "class") { |
| 99 | + attr2 = el2.attributes.getNamedItem(name); |
| 100 | + if (attr1.specified != attr2.specified) return false; |
| 101 | + if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false; |
| 102 | + } |
| 103 | + } |
| 104 | + return true; |
| 105 | + } |
| 106 | + |
| 107 | + function elementHasNonClassAttributes(el, exceptions) { |
| 108 | + for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) { |
| 109 | + attrName = el.attributes[i].name; |
| 110 | + if ( !(exceptions && dom.arrayContains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") { |
| 111 | + return true; |
| 112 | + } |
| 113 | + } |
| 114 | + return false; |
| 115 | + } |
| 116 | + |
| 117 | + function elementHasProps(el, props) { |
| 118 | + for (var p in props) { |
| 119 | + if (props.hasOwnProperty(p) && el[p] !== props[p]) { |
| 120 | + return false; |
| 121 | + } |
| 122 | + } |
| 123 | + return true; |
| 124 | + } |
| 125 | + |
| 126 | + var getComputedStyleProperty; |
| 127 | + |
| 128 | + if (typeof window.getComputedStyle != "undefined") { |
| 129 | + getComputedStyleProperty = function(el, propName) { |
| 130 | + return dom.getWindow(el).getComputedStyle(el, null)[propName]; |
| 131 | + }; |
| 132 | + } else if (typeof document.documentElement.currentStyle != "undefined") { |
| 133 | + getComputedStyleProperty = function(el, propName) { |
| 134 | + return el.currentStyle[propName]; |
| 135 | + }; |
| 136 | + } else { |
| 137 | + module.fail("No means of obtaining computed style properties found"); |
| 138 | + } |
| 139 | + |
| 140 | + var isEditableElement; |
| 141 | + |
| 142 | + (function() { |
| 143 | + var testEl = document.createElement("div"); |
| 144 | + if (typeof testEl.isContentEditable == "boolean") { |
| 145 | + isEditableElement = function(node) { |
| 146 | + return node && node.nodeType == 1 && node.isContentEditable; |
| 147 | + }; |
| 148 | + } else { |
| 149 | + isEditableElement = function(node) { |
| 150 | + if (!node || node.nodeType != 1 || node.contentEditable == "false") { |
| 151 | + return false; |
| 152 | + } |
| 153 | + return node.contentEditable == "true" || isEditableElement(node.parentNode); |
| 154 | + }; |
| 155 | + } |
| 156 | + })(); |
| 157 | + |
| 158 | + function isEditingHost(node) { |
| 159 | + var parent; |
| 160 | + return node && node.nodeType == 1 |
| 161 | + && (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") |
| 162 | + || (isEditableElement(node) && !isEditableElement(node.parentNode))); |
| 163 | + } |
| 164 | + |
| 165 | + function isEditable(node) { |
| 166 | + return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node); |
| 167 | + } |
| 168 | + |
| 169 | + var inlineDisplayRegex = /^inline(-block|-table)?$/i; |
| 170 | + |
| 171 | + function isNonInlineElement(node) { |
| 172 | + return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display")); |
| 173 | + } |
| 174 | + |
| 175 | + // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html) |
| 176 | + var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/; |
| 177 | + |
| 178 | + function isUnrenderedWhiteSpaceNode(node) { |
| 179 | + if (node.data.length == 0) { |
| 180 | + return true; |
| 181 | + } |
| 182 | + if (htmlNonWhiteSpaceRegex.test(node.data)) { |
| 183 | + return false; |
| 184 | + } |
| 185 | + var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); |
| 186 | + switch (cssWhiteSpace) { |
| 187 | + case "pre": |
| 188 | + case "pre-wrap": |
| 189 | + case "-moz-pre-wrap": |
| 190 | + return false; |
| 191 | + case "pre-line": |
| 192 | + if (/[\r\n]/.test(node.data)) { |
| 193 | + return false; |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a |
| 198 | + // non-inline element, it will not be rendered. This seems to be a good enough definition. |
| 199 | + return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling); |
| 200 | + } |
| 201 | + |
| 202 | + function isSplitPoint(node, offset) { |
| 203 | + if (dom.isCharacterDataNode(node)) { |
| 204 | + if (offset == 0) { |
| 205 | + return !!node.previousSibling; |
| 206 | + } else if (offset == node.length) { |
| 207 | + return !!node.nextSibling; |
| 208 | + } else { |
| 209 | + return true; |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + return offset > 0 && offset < node.childNodes.length; |
| 214 | + } |
| 215 | + |
| 216 | + function splitNodeAt(node, descendantNode, descendantOffset, rangesToPreserve) { |
| 217 | + var newNode; |
| 218 | + var splitAtStart = (descendantOffset == 0); |
| 219 | + |
| 220 | + if (dom.isAncestorOf(descendantNode, node)) { |
| 221 | + |
| 222 | + return node; |
| 223 | + } |
| 224 | + |
| 225 | + if (dom.isCharacterDataNode(descendantNode)) { |
| 226 | + if (descendantOffset == 0) { |
| 227 | + descendantOffset = dom.getNodeIndex(descendantNode); |
| 228 | + descendantNode = descendantNode.parentNode; |
| 229 | + } else if (descendantOffset == descendantNode.length) { |
| 230 | + descendantOffset = dom.getNodeIndex(descendantNode) + 1; |
| 231 | + descendantNode = descendantNode.parentNode; |
| 232 | + } else { |
| 233 | + throw module.createError("splitNodeAt should not be called with offset in the middle of a data node (" |
| 234 | + + descendantOffset + " in " + descendantNode.data); |
| 235 | + } |
| 236 | + } |
| 237 | + |
| 238 | + if (isSplitPoint(descendantNode, descendantOffset)) { |
| 239 | + if (!newNode) { |
| 240 | + newNode = descendantNode.cloneNode(false); |
| 241 | + if (newNode.id) { |
| 242 | + newNode.removeAttribute("id"); |
| 243 | + } |
| 244 | + var child; |
| 245 | + while ((child = descendantNode.childNodes[descendantOffset])) { |
| 246 | + newNode.appendChild(child); |
| 247 | + } |
| 248 | + dom.insertAfter(newNode, descendantNode); |
| 249 | + } |
| 250 | + return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve); |
| 251 | + } else if (node != descendantNode) { |
| 252 | + newNode = descendantNode.parentNode; |
| 253 | + |
| 254 | + // Work out a new split point in the parent node |
| 255 | + var newNodeIndex = dom.getNodeIndex(descendantNode); |
| 256 | + |
| 257 | + if (!splitAtStart) { |
| 258 | + newNodeIndex++; |
| 259 | + } |
| 260 | + return splitNodeAt(node, newNode, newNodeIndex, rangesToPreserve); |
| 261 | + } |
| 262 | + return node; |
| 263 | + } |
| 264 | + |
| 265 | + function areElementsMergeable(el1, el2) { |
| 266 | + return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2); |
| 267 | + } |
| 268 | + |
| 269 | + function createAdjacentMergeableTextNodeGetter(forward) { |
| 270 | + var propName = forward ? "nextSibling" : "previousSibling"; |
| 271 | + |
| 272 | + return function(textNode, checkParentElement) { |
| 273 | + var el = textNode.parentNode; |
| 274 | + var adjacentNode = textNode[propName]; |
| 275 | + if (adjacentNode) { |
| 276 | + // Can merge if the node's previous/next sibling is a text node |
| 277 | + if (adjacentNode && adjacentNode.nodeType == 3) { |
| 278 | + return adjacentNode; |
| 279 | + } |
| 280 | + } else if (checkParentElement) { |
| 281 | + // Compare text node parent element with its sibling |
| 282 | + adjacentNode = el[propName]; |
| 283 | + |
| 284 | + if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) { |
| 285 | + return adjacentNode[forward ? "firstChild" : "lastChild"]; |
| 286 | + } |
| 287 | + } |
| 288 | + return null; |
| 289 | + } |
| 290 | + } |
| 291 | + |
| 292 | + var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false), |
| 293 | + getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true); |
| 294 | + |
| 295 | + |
| 296 | + function Merge(firstNode) { |
| 297 | + this.isElementMerge = (firstNode.nodeType == 1); |
| 298 | + this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; |
| 299 | + this.textNodes = [this.firstTextNode]; |
| 300 | + } |
| 301 | + |
| 302 | + Merge.prototype = { |
| 303 | + doMerge: function() { |
| 304 | + var textBits = [], textNode, parent, text; |
| 305 | + for (var i = 0, len = this.textNodes.length; i < len; ++i) { |
| 306 | + textNode = this.textNodes[i]; |
| 307 | + parent = textNode.parentNode; |
| 308 | + textBits[i] = textNode.data; |
| 309 | + if (i) { |
| 310 | + parent.removeChild(textNode); |
| 311 | + if (!parent.hasChildNodes()) { |
| 312 | + parent.parentNode.removeChild(parent); |
| 313 | + } |
| 314 | + } |
| 315 | + } |
| 316 | + this.firstTextNode.data = text = textBits.join(""); |
| 317 | + return text; |
| 318 | + }, |
| 319 | + |
| 320 | + getLength: function() { |
| 321 | + var i = this.textNodes.length, len = 0; |
| 322 | + while (i--) { |
| 323 | + len += this.textNodes[i].length; |
| 324 | + } |
| 325 | + return len; |
| 326 | + }, |
| 327 | + |
| 328 | + toString: function() { |
| 329 | + var textBits = []; |
| 330 | + for (var i = 0, len = this.textNodes.length; i < len; ++i) { |
| 331 | + textBits[i] = "'" + this.textNodes[i].data + "'"; |
| 332 | + } |
| 333 | + return "[Merge(" + textBits.join(",") + ")]"; |
| 334 | + } |
| 335 | + }; |
| 336 | + |
| 337 | + var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"]; |
| 338 | + |
| 339 | + // Allow "class" as a property name in object properties |
| 340 | + var mappedPropertyNames = {"class" : "className"}; |
| 341 | + |
| 342 | + function CssClassApplier(cssClass, options, tagNames) { |
| 343 | + this.cssClass = cssClass; |
| 344 | + var normalize, i, len, propName; |
| 345 | + |
| 346 | + var elementPropertiesFromOptions = null; |
| 347 | + |
| 348 | + // Initialize from options object |
| 349 | + if (typeof options == "object" && options !== null) { |
| 350 | + tagNames = options.tagNames; |
| 351 | + elementPropertiesFromOptions = options.elementProperties; |
| 352 | + |
| 353 | + for (i = 0; propName = optionProperties[i++]; ) { |
| 354 | + if (options.hasOwnProperty(propName)) { |
| 355 | + this[propName] = options[propName]; |
| 356 | + } |
| 357 | + } |
| 358 | + normalize = options.normalize; |
| 359 | + } else { |
| 360 | + normalize = options; |
| 361 | + } |
| 362 | + |
| 363 | + // Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization |
| 364 | + this.normalize = (typeof normalize == "undefined") ? true : normalize; |
| 365 | + |
| 366 | + // Initialize element properties and attribute exceptions |
| 367 | + this.attrExceptions = []; |
| 368 | + var el = document.createElement(this.elementTagName); |
| 369 | + this.elementProperties = {}; |
| 370 | + for (var p in elementPropertiesFromOptions) { |
| 371 | + if (elementPropertiesFromOptions.hasOwnProperty(p)) { |
| 372 | + // Map "class" to "className" |
| 373 | + if (mappedPropertyNames.hasOwnProperty(p)) { |
| 374 | + p = mappedPropertyNames[p]; |
| 375 | + } |
| 376 | + el[p] = elementPropertiesFromOptions[p]; |
| 377 | + |
| 378 | + // Copy the property back from the dummy element so that later comparisons to check whether elements |
| 379 | + // may be removed are checking against the right value. For example, the href property of an element |
| 380 | + // returns a fully qualified URL even if it was previously assigned a relative URL. |
| 381 | + this.elementProperties[p] = el[p]; |
| 382 | + this.attrExceptions.push(p); |
| 383 | + } |
| 384 | + } |
| 385 | + |
| 386 | + this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ? |
| 387 | + sortClassName(this.elementProperties.className + " " + cssClass) : cssClass; |
| 388 | + |
| 389 | + // Initialize tag names |
| 390 | + this.applyToAnyTagName = false; |
| 391 | + var type = typeof tagNames; |
| 392 | + if (type == "string") { |
| 393 | + if (tagNames == "*") { |
| 394 | + this.applyToAnyTagName = true; |
| 395 | + } else { |
| 396 | + this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/); |
| 397 | + } |
| 398 | + } else if (type == "object" && typeof tagNames.length == "number") { |
| 399 | + this.tagNames = []; |
| 400 | + for (i = 0, len = tagNames.length; i < len; ++i) { |
| 401 | + if (tagNames[i] == "*") { |
| 402 | + this.applyToAnyTagName = true; |
| 403 | + } else { |
| 404 | + this.tagNames.push(tagNames[i].toLowerCase()); |
| 405 | + } |
| 406 | + } |
| 407 | + } else { |
| 408 | + this.tagNames = [this.elementTagName]; |
| 409 | + } |
| 410 | + } |
| 411 | + |
| 412 | + CssClassApplier.prototype = { |
| 413 | + elementTagName: defaultTagName, |
| 414 | + elementProperties: {}, |
| 415 | + ignoreWhiteSpace: true, |
| 416 | + applyToEditableOnly: false, |
| 417 | + |
| 418 | + hasClass: function(node) { |
| 419 | + return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass); |
| 420 | + }, |
| 421 | + |
| 422 | + getSelfOrAncestorWithClass: function(node) { |
| 423 | + while (node) { |
| 424 | + if (this.hasClass(node, this.cssClass)) { |
| 425 | + return node; |
| 426 | + } |
| 427 | + node = node.parentNode; |
| 428 | + } |
| 429 | + return null; |
| 430 | + }, |
| 431 | + |
| 432 | + isModifiable: function(node) { |
| 433 | + return !this.applyToEditableOnly || isEditable(node); |
| 434 | + }, |
| 435 | + |
| 436 | + // White space adjacent to an unwrappable node can be ignored for wrapping |
| 437 | + isIgnorableWhiteSpaceNode: function(node) { |
| 438 | + return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node); |
| 439 | + }, |
| 440 | + |
| 441 | + // Normalizes nodes after applying a CSS class to a Range. |
| 442 | + postApply: function(textNodes, range, isUndo) { |
| 443 | + |
| 444 | + var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; |
| 445 | + |
| 446 | + var merges = [], currentMerge; |
| 447 | + |
| 448 | + var rangeStartNode = firstNode, rangeEndNode = lastNode; |
| 449 | + var rangeStartOffset = 0, rangeEndOffset = lastNode.length; |
| 450 | + |
| 451 | + var textNode, precedingTextNode; |
| 452 | + |
| 453 | + for (var i = 0, len = textNodes.length; i < len; ++i) { |
| 454 | + textNode = textNodes[i]; |
| 455 | + precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo); |
| 456 | + |
| 457 | + if (precedingTextNode) { |
| 458 | + if (!currentMerge) { |
| 459 | + currentMerge = new Merge(precedingTextNode); |
| 460 | + merges.push(currentMerge); |
| 461 | + } |
| 462 | + currentMerge.textNodes.push(textNode); |
| 463 | + if (textNode === firstNode) { |
| 464 | + rangeStartNode = currentMerge.firstTextNode; |
| 465 | + rangeStartOffset = rangeStartNode.length; |
| 466 | + } |
| 467 | + if (textNode === lastNode) { |
| 468 | + rangeEndNode = currentMerge.firstTextNode; |
| 469 | + rangeEndOffset = currentMerge.getLength(); |
| 470 | + } |
| 471 | + } else { |
| 472 | + currentMerge = null; |
| 473 | + } |
| 474 | + } |
| 475 | + |
| 476 | + // Test whether the first node after the range needs merging |
| 477 | + var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo); |
| 478 | + |
| 479 | + if (nextTextNode) { |
| 480 | + if (!currentMerge) { |
| 481 | + currentMerge = new Merge(lastNode); |
| 482 | + merges.push(currentMerge); |
| 483 | + } |
| 484 | + currentMerge.textNodes.push(nextTextNode); |
| 485 | + } |
| 486 | + |
| 487 | + // Do the merges |
| 488 | + if (merges.length) { |
| 489 | + |
| 490 | + for (i = 0, len = merges.length; i < len; ++i) { |
| 491 | + merges[i].doMerge(); |
| 492 | + } |
| 493 | + |
| 494 | + |
| 495 | + // Set the range boundaries |
| 496 | + range.setStart(rangeStartNode, rangeStartOffset); |
| 497 | + range.setEnd(rangeEndNode, rangeEndOffset); |
| 498 | + } |
| 499 | + |
| 500 | + }, |
| 501 | + |
| 502 | + createContainer: function(doc) { |
| 503 | + var el = doc.createElement(this.elementTagName); |
| 504 | + api.util.extend(el, this.elementProperties); |
| 505 | + addClass(el, this.cssClass); |
| 506 | + return el; |
| 507 | + }, |
| 508 | + |
| 509 | + applyToTextNode: function(textNode) { |
| 510 | + |
| 511 | + |
| 512 | + var parent = textNode.parentNode; |
| 513 | + if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { |
| 514 | + addClass(parent, this.cssClass); |
| 515 | + } else { |
| 516 | + var el = this.createContainer(dom.getDocument(textNode)); |
| 517 | + textNode.parentNode.insertBefore(el, textNode); |
| 518 | + el.appendChild(textNode); |
| 519 | + } |
| 520 | + |
| 521 | + }, |
| 522 | + |
| 523 | + isRemovable: function(el) { |
| 524 | + return el.tagName.toLowerCase() == this.elementTagName |
| 525 | + && getSortedClassName(el) == this.elementSortedClassName |
| 526 | + && elementHasProps(el, this.elementProperties) |
| 527 | + && !elementHasNonClassAttributes(el, this.attrExceptions) |
| 528 | + && this.isModifiable(el); |
| 529 | + }, |
| 530 | + |
| 531 | + undoToTextNode: function(textNode, range, ancestorWithClass) { |
| 532 | + |
| 533 | + if (!range.containsNode(ancestorWithClass)) { |
| 534 | + // Split out the portion of the ancestor from which we can remove the CSS class |
| 535 | + //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass); |
| 536 | + var ancestorRange = range.cloneRange(); |
| 537 | + ancestorRange.selectNode(ancestorWithClass); |
| 538 | + |
| 539 | + if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) { |
| 540 | + splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]); |
| 541 | + range.setEndAfter(ancestorWithClass); |
| 542 | + } |
| 543 | + if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) { |
| 544 | + ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]); |
| 545 | + } |
| 546 | + } |
| 547 | + |
| 548 | + if (this.isRemovable(ancestorWithClass)) { |
| 549 | + replaceWithOwnChildren(ancestorWithClass); |
| 550 | + } else { |
| 551 | + removeClass(ancestorWithClass, this.cssClass); |
| 552 | + } |
| 553 | + }, |
| 554 | + |
| 555 | + applyToRange: function(range) { |
| 556 | + range.splitBoundaries(); |
| 557 | + var textNodes = getEffectiveTextNodes(range); |
| 558 | + |
| 559 | + if (textNodes.length) { |
| 560 | + var textNode; |
| 561 | + |
| 562 | + for (var i = 0, len = textNodes.length; i < len; ++i) { |
| 563 | + textNode = textNodes[i]; |
| 564 | + |
| 565 | + if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode) |
| 566 | + && this.isModifiable(textNode)) { |
| 567 | + this.applyToTextNode(textNode); |
| 568 | + } |
| 569 | + } |
| 570 | + range.setStart(textNodes[0], 0); |
| 571 | + textNode = textNodes[textNodes.length - 1]; |
| 572 | + range.setEnd(textNode, textNode.length); |
| 573 | + if (this.normalize) { |
| 574 | + this.postApply(textNodes, range, false); |
| 575 | + } |
| 576 | + } |
| 577 | + }, |
| 578 | + |
| 579 | + applyToSelection: function(win) { |
| 580 | + |
| 581 | + win = win || window; |
| 582 | + var sel = api.getSelection(win); |
| 583 | + |
| 584 | + var range, ranges = sel.getAllRanges(); |
| 585 | + sel.removeAllRanges(); |
| 586 | + var i = ranges.length; |
| 587 | + while (i--) { |
| 588 | + range = ranges[i]; |
| 589 | + this.applyToRange(range); |
| 590 | + sel.addRange(range); |
| 591 | + } |
| 592 | + |
| 593 | + }, |
| 594 | + |
| 595 | + undoToRange: function(range) { |
| 596 | + |
| 597 | + range.splitBoundaries(); |
| 598 | + var textNodes = getEffectiveTextNodes(range); |
| 599 | + var textNode, ancestorWithClass; |
| 600 | + var lastTextNode = textNodes[textNodes.length - 1]; |
| 601 | + |
| 602 | + if (textNodes.length) { |
| 603 | + for (var i = 0, len = textNodes.length; i < len; ++i) { |
| 604 | + textNode = textNodes[i]; |
| 605 | + ancestorWithClass = this.getSelfOrAncestorWithClass(textNode); |
| 606 | + if (ancestorWithClass && this.isModifiable(textNode)) { |
| 607 | + this.undoToTextNode(textNode, range, ancestorWithClass); |
| 608 | + } |
| 609 | + |
| 610 | + // Ensure the range is still valid |
| 611 | + range.setStart(textNodes[0], 0); |
| 612 | + range.setEnd(lastTextNode, lastTextNode.length); |
| 613 | + } |
| 614 | + |
| 615 | + |
| 616 | + |
| 617 | + if (this.normalize) { |
| 618 | + this.postApply(textNodes, range, true); |
| 619 | + } |
| 620 | + } |
| 621 | + }, |
| 622 | + |
| 623 | + undoToSelection: function(win) { |
| 624 | + win = win || window; |
| 625 | + var sel = api.getSelection(win); |
| 626 | + var ranges = sel.getAllRanges(), range; |
| 627 | + sel.removeAllRanges(); |
| 628 | + for (var i = 0, len = ranges.length; i < len; ++i) { |
| 629 | + range = ranges[i]; |
| 630 | + this.undoToRange(range); |
| 631 | + sel.addRange(range); |
| 632 | + } |
| 633 | + }, |
| 634 | + |
| 635 | + getTextSelectedByRange: function(textNode, range) { |
| 636 | + var textRange = range.cloneRange(); |
| 637 | + textRange.selectNodeContents(textNode); |
| 638 | + |
| 639 | + var intersectionRange = textRange.intersection(range); |
| 640 | + var text = intersectionRange ? intersectionRange.toString() : ""; |
| 641 | + textRange.detach(); |
| 642 | + |
| 643 | + return text; |
| 644 | + }, |
| 645 | + |
| 646 | + isAppliedToRange: function(range) { |
| 647 | + if (range.collapsed) { |
| 648 | + return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer); |
| 649 | + } else { |
| 650 | + var textNodes = range.getNodes( [3] ); |
| 651 | + for (var i = 0, textNode; textNode = textNodes[i++]; ) { |
| 652 | + if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) |
| 653 | + && this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) { |
| 654 | + return false; |
| 655 | + } |
| 656 | + } |
| 657 | + return true; |
| 658 | + } |
| 659 | + }, |
| 660 | + |
| 661 | + isAppliedToSelection: function(win) { |
| 662 | + win = win || window; |
| 663 | + var sel = api.getSelection(win); |
| 664 | + var ranges = sel.getAllRanges(); |
| 665 | + var i = ranges.length; |
| 666 | + while (i--) { |
| 667 | + if (!this.isAppliedToRange(ranges[i])) { |
| 668 | + return false; |
| 669 | + } |
| 670 | + } |
| 671 | + |
| 672 | + return true; |
| 673 | + }, |
| 674 | + |
| 675 | + toggleRange: function(range) { |
| 676 | + if (this.isAppliedToRange(range)) { |
| 677 | + this.undoToRange(range); |
| 678 | + } else { |
| 679 | + this.applyToRange(range); |
| 680 | + } |
| 681 | + }, |
| 682 | + |
| 683 | + toggleSelection: function(win) { |
| 684 | + if (this.isAppliedToSelection(win)) { |
| 685 | + this.undoToSelection(win); |
| 686 | + } else { |
| 687 | + this.applyToSelection(win); |
| 688 | + } |
| 689 | + }, |
| 690 | + |
| 691 | + detach: function() {} |
| 692 | + }; |
| 693 | + |
| 694 | + function createCssClassApplier(cssClass, options, tagNames) { |
| 695 | + return new CssClassApplier(cssClass, options, tagNames); |
| 696 | + } |
| 697 | + |
| 698 | + CssClassApplier.util = { |
| 699 | + hasClass: hasClass, |
| 700 | + addClass: addClass, |
| 701 | + removeClass: removeClass, |
| 702 | + hasSameClasses: haveSameClasses, |
| 703 | + replaceWithOwnChildren: replaceWithOwnChildren, |
| 704 | + elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes, |
| 705 | + elementHasNonClassAttributes: elementHasNonClassAttributes, |
| 706 | + splitNodeAt: splitNodeAt, |
| 707 | + isEditableElement: isEditableElement, |
| 708 | + isEditingHost: isEditingHost, |
| 709 | + isEditable: isEditable |
| 710 | + }; |
| 711 | + |
| 712 | + api.CssClassApplier = CssClassApplier; |
| 713 | + api.createCssClassApplier = createCssClassApplier; |
| 714 | +}); |
Property changes on: trunk/extensions/VisualEditor/contentEditable/rangy/rangy-cssclassapplier.js |
___________________________________________________________________ |
Added: svn:eol-style |
715 | 715 | + native |
Property changes on: trunk/extensions/VisualEditor/contentEditable/index.php |
___________________________________________________________________ |
Added: svn:eol-style |
716 | 716 | + native |
Index: trunk/extensions/VisualEditor/contentEditable/views/es.ParagraphView.js |
— | — | @@ -1,26 +1,26 @@ |
2 | | -/**
|
3 | | - * Creates an es.ParagraphView object.
|
4 | | - *
|
5 | | - * @class
|
6 | | - * @constructor
|
7 | | - * @extends {es.DocumentViewLeafNode}
|
8 | | - * @param {es.ParagraphModel} model Paragraph model to view
|
9 | | - */
|
10 | | -es.ParagraphView = function( model ) {
|
11 | | - // Inheritance
|
12 | | - es.DocumentViewLeafNode.call( this, model );
|
13 | | -
|
14 | | - // DOM Changes
|
15 | | - this.$.addClass( 'es-paragraphView' );
|
16 | | -};
|
17 | | -
|
18 | | -/* Registration */
|
19 | | -
|
20 | | -es.DocumentView.splitRules.paragraph = {
|
21 | | - 'self': true,
|
22 | | - 'children': null
|
23 | | -};
|
24 | | -
|
25 | | -/* Inheritance */
|
26 | | -
|
27 | | -es.extendClass( es.ParagraphView, es.DocumentViewLeafNode );
|
| 2 | +/** |
| 3 | + * Creates an es.ParagraphView object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @extends {es.DocumentViewLeafNode} |
| 8 | + * @param {es.ParagraphModel} model Paragraph model to view |
| 9 | + */ |
| 10 | +es.ParagraphView = function( model ) { |
| 11 | + // Inheritance |
| 12 | + es.DocumentViewLeafNode.call( this, model ); |
| 13 | + |
| 14 | + // DOM Changes |
| 15 | + this.$.addClass( 'es-paragraphView' ); |
| 16 | +}; |
| 17 | + |
| 18 | +/* Registration */ |
| 19 | + |
| 20 | +es.DocumentView.splitRules.paragraph = { |
| 21 | + 'self': true, |
| 22 | + 'children': null |
| 23 | +}; |
| 24 | + |
| 25 | +/* Inheritance */ |
| 26 | + |
| 27 | +es.extendClass( es.ParagraphView, es.DocumentViewLeafNode ); |
Property changes on: trunk/extensions/VisualEditor/contentEditable/views/es.ParagraphView.js |
___________________________________________________________________ |
Added: svn:eol-style |
28 | 28 | + native |
Property changes on: trunk/extensions/VisualEditor/contentEditable/views/es.SurfaceView.js |
___________________________________________________________________ |
Added: svn:eol-style |
29 | 29 | + native |
Index: trunk/extensions/VisualEditor/contentEditable/views/es.ContentView.js |
— | — | @@ -1,244 +1,244 @@ |
2 | | -es.ContentView = function( $container, model ) {
|
3 | | - // Inheritance
|
4 | | - es.EventEmitter.call( this );
|
5 | | -
|
6 | | - // Properties
|
7 | | - this.$ = $container;
|
8 | | - this.model = model;
|
9 | | -
|
10 | | - if ( model ) {
|
11 | | - // Events
|
12 | | - var _this = this;
|
13 | | - this.model.on( 'update', function( offset ) {
|
14 | | - _this.render( offset || 0 );
|
15 | | - } );
|
16 | | - }
|
17 | | -}
|
18 | | -
|
19 | | -
|
20 | | -/* Static Members */
|
21 | | -
|
22 | | -/**
|
23 | | - * List of annotation rendering implementations.
|
24 | | - *
|
25 | | - * Each supported annotation renderer must have an open and close property, each either a string or
|
26 | | - * a function which accepts a data argument.
|
27 | | - *
|
28 | | - * @static
|
29 | | - * @member
|
30 | | - */
|
31 | | -es.ContentView.annotationRenderers = {
|
32 | | - 'object/template': {
|
33 | | - 'open': function( data ) {
|
34 | | - return '<span class="es-contentView-format-object" contentEditable="false">' + data.html;
|
35 | | - },
|
36 | | - 'close': '</span>'
|
37 | | - },
|
38 | | - 'object/hook': {
|
39 | | - 'open': function( data ) {
|
40 | | - return '<span class="es-contentView-format-object">' + data.html;
|
41 | | - },
|
42 | | - 'close': '</span>'
|
43 | | - },
|
44 | | - 'textStyle/bold': {
|
45 | | - //'open': '<span class="es-contentView-format-textStyle-bold">',
|
46 | | - //'close': '</span>'
|
47 | | - 'open': '<b>',
|
48 | | - 'close': '</b>'
|
49 | | - },
|
50 | | - 'textStyle/italic': {
|
51 | | - 'open': '<span class="es-contentView-format-textStyle-italic">',
|
52 | | - 'close': '</span>'
|
53 | | - },
|
54 | | - 'textStyle/strong': {
|
55 | | - 'open': '<span class="es-contentView-format-textStyle-strong">',
|
56 | | - 'close': '</span>'
|
57 | | - },
|
58 | | - 'textStyle/emphasize': {
|
59 | | - 'open': '<span class="es-contentView-format-textStyle-emphasize">',
|
60 | | - 'close': '</span>'
|
61 | | - },
|
62 | | - 'textStyle/big': {
|
63 | | - 'open': '<span class="es-contentView-format-textStyle-big">',
|
64 | | - 'close': '</span>'
|
65 | | - },
|
66 | | - 'textStyle/small': {
|
67 | | - 'open': '<span class="es-contentView-format-textStyle-small">',
|
68 | | - 'close': '</span>'
|
69 | | - },
|
70 | | - 'textStyle/superScript': {
|
71 | | - 'open': '<span class="es-contentView-format-textStyle-superScript">',
|
72 | | - 'close': '</span>'
|
73 | | - },
|
74 | | - 'textStyle/subScript': {
|
75 | | - 'open': '<span class="es-contentView-format-textStyle-subScript">',
|
76 | | - 'close': '</span>'
|
77 | | - },
|
78 | | - 'link/external': {
|
79 | | - 'open': function( data ) {
|
80 | | - return '<span class="es-contentView-format-link" data-href="' + data.href + '">';
|
81 | | - },
|
82 | | - 'close': '</span>'
|
83 | | - },
|
84 | | - 'link/internal': {
|
85 | | - 'open': function( data ) {
|
86 | | - return '<span class="es-contentView-format-link" data-title="wiki/' + data.title + '">';
|
87 | | - },
|
88 | | - 'close': '</span>'
|
89 | | - }
|
90 | | -};
|
91 | | -
|
92 | | -/**
|
93 | | - * Mapping of character and HTML entities or renderings.
|
94 | | - *
|
95 | | - * @static
|
96 | | - * @member
|
97 | | - */
|
98 | | -es.ContentView.htmlCharacters = {
|
99 | | - '&': '&',
|
100 | | - '<': '<',
|
101 | | - '>': '>',
|
102 | | - '\'': ''',
|
103 | | - '"': '"',
|
104 | | - '\n': '<span class="es-contentView-whitespace">¶</span>',
|
105 | | - '\t': '<span class="es-contentView-whitespace">⇾</span>',
|
106 | | - ' ': ' '
|
107 | | -};
|
108 | | -
|
109 | | -/* Static Methods */
|
110 | | -
|
111 | | -/**
|
112 | | - * Gets a rendered opening or closing of an annotation.
|
113 | | - *
|
114 | | - * Tag nesting is handled using a stack, which keeps track of what is currently open. A common stack
|
115 | | - * argument should be used while rendering content.
|
116 | | - *
|
117 | | - * @static
|
118 | | - * @method
|
119 | | - * @param {String} bias Which side of the annotation to render, either "open" or "close"
|
120 | | - * @param {Object} annotation Annotation to render
|
121 | | - * @param {Array} stack List of currently open annotations
|
122 | | - * @returns {String} Rendered annotation
|
123 | | - */
|
124 | | -es.ContentView.renderAnnotation = function( bias, annotation, stack ) {
|
125 | | - var renderers = es.ContentView.annotationRenderers,
|
126 | | - type = annotation.type,
|
127 | | - out = '';
|
128 | | - if ( type in renderers ) {
|
129 | | - if ( bias === 'open' ) {
|
130 | | - // Add annotation to the top of the stack
|
131 | | - stack.push( annotation );
|
132 | | - // Open annotation
|
133 | | - out += typeof renderers[type].open === 'function' ?
|
134 | | - renderers[type].open( annotation.data ) : renderers[type].open;
|
135 | | - } else {
|
136 | | - if ( stack[stack.length - 1] === annotation ) {
|
137 | | - // Remove annotation from top of the stack
|
138 | | - stack.pop();
|
139 | | - // Close annotation
|
140 | | - out += typeof renderers[type].close === 'function' ?
|
141 | | - renderers[type].close( annotation.data ) : renderers[type].close;
|
142 | | - } else {
|
143 | | - // Find the annotation in the stack
|
144 | | - var depth = es.inArray( annotation, stack ),
|
145 | | - i;
|
146 | | - if ( depth === -1 ) {
|
147 | | - throw 'Invalid stack error. An element is missing from the stack.';
|
148 | | - }
|
149 | | - // Close each already opened annotation
|
150 | | - for ( i = stack.length - 1; i >= depth + 1; i-- ) {
|
151 | | - out += typeof renderers[stack[i].type].close === 'function' ?
|
152 | | - renderers[stack[i].type].close( stack[i].data ) :
|
153 | | - renderers[stack[i].type].close;
|
154 | | - }
|
155 | | - // Close the buried annotation
|
156 | | - out += typeof renderers[type].close === 'function' ?
|
157 | | - renderers[type].close( annotation.data ) : renderers[type].close;
|
158 | | - // Re-open each previously opened annotation
|
159 | | - for ( i = depth + 1; i < stack.length; i++ ) {
|
160 | | - out += typeof renderers[stack[i].type].open === 'function' ?
|
161 | | - renderers[stack[i].type].open( stack[i].data ) :
|
162 | | - renderers[stack[i].type].open;
|
163 | | - }
|
164 | | - // Remove the annotation from the middle of the stack
|
165 | | - stack.splice( depth, 1 );
|
166 | | - }
|
167 | | - }
|
168 | | - }
|
169 | | - return out;
|
170 | | -};
|
171 | | -
|
172 | | -/* Methods */
|
173 | | -
|
174 | | -es.ContentView.prototype.render = function( offset ) {
|
175 | | - this.$.html(this.getHtml(0, this.model.getContentLength()));
|
176 | | -};
|
177 | | -
|
178 | | -/**
|
179 | | - * Gets an HTML rendering of a range of data within content model.
|
180 | | - *
|
181 | | - * @method
|
182 | | - * @param {es.Range} range Range of content to render
|
183 | | - * @param {String} Rendered HTML of data within content model
|
184 | | - */
|
185 | | -es.ContentView.prototype.getHtml = function( range, options ) {
|
186 | | - if ( range ) {
|
187 | | - range.normalize();
|
188 | | - } else {
|
189 | | - range = { 'start': 0, 'end': undefined };
|
190 | | - }
|
191 | | - var data = this.model.getContentData(),
|
192 | | - render = es.ContentView.renderAnnotation,
|
193 | | - htmlChars = es.ContentView.htmlCharacters;
|
194 | | - var out = '',
|
195 | | - left = '',
|
196 | | - right,
|
197 | | - leftPlain,
|
198 | | - rightPlain,
|
199 | | - stack = [],
|
200 | | - chr,
|
201 | | - i,
|
202 | | - j;
|
203 | | - for ( i = 0; i < data.length; i++ ) {
|
204 | | - right = data[i];
|
205 | | - leftPlain = typeof left === 'string';
|
206 | | - rightPlain = typeof right === 'string';
|
207 | | - if ( !leftPlain && rightPlain ) {
|
208 | | - // [formatted][plain] pair, close any annotations for left
|
209 | | - for ( j = 1; j < left.length; j++ ) {
|
210 | | - out += render( 'close', left[j], stack );
|
211 | | - }
|
212 | | - } else if ( leftPlain && !rightPlain ) {
|
213 | | - // [plain][formatted] pair, open any annotations for right
|
214 | | - for ( j = 1; j < right.length; j++ ) {
|
215 | | - out += render( 'open', right[j], stack );
|
216 | | - }
|
217 | | - } else if ( !leftPlain && !rightPlain ) {
|
218 | | - // [formatted][formatted] pair, open/close any differences
|
219 | | - for ( j = 1; j < left.length; j++ ) {
|
220 | | - if ( es.inArray( left[j], right ) === -1 ) {
|
221 | | - out += render( 'close', left[j], stack );
|
222 | | - }
|
223 | | - }
|
224 | | - for ( j = 1; j < right.length; j++ ) {
|
225 | | - if ( es.inArray( right[j], left ) === -1 ) {
|
226 | | - out += render( 'open', right[j], stack );
|
227 | | - }
|
228 | | - }
|
229 | | - }
|
230 | | - chr = rightPlain ? right : right[0];
|
231 | | - out += chr in htmlChars ? htmlChars[chr] : chr;
|
232 | | - left = right;
|
233 | | - }
|
234 | | - // Close all remaining tags at the end of the content
|
235 | | - if ( !rightPlain && right ) {
|
236 | | - for ( j = 1; j < right.length; j++ ) {
|
237 | | - out += render( 'close', right[j], stack );
|
238 | | - }
|
239 | | - }
|
240 | | - return out;
|
241 | | -};
|
242 | | -
|
243 | | -/* Inheritance */
|
244 | | -
|
245 | | -es.extendClass( es.ContentView, es.EventEmitter );
|
| 2 | +es.ContentView = function( $container, model ) { |
| 3 | + // Inheritance |
| 4 | + es.EventEmitter.call( this ); |
| 5 | + |
| 6 | + // Properties |
| 7 | + this.$ = $container; |
| 8 | + this.model = model; |
| 9 | + |
| 10 | + if ( model ) { |
| 11 | + // Events |
| 12 | + var _this = this; |
| 13 | + this.model.on( 'update', function( offset ) { |
| 14 | + _this.render( offset || 0 ); |
| 15 | + } ); |
| 16 | + } |
| 17 | +} |
| 18 | + |
| 19 | + |
| 20 | +/* Static Members */ |
| 21 | + |
| 22 | +/** |
| 23 | + * List of annotation rendering implementations. |
| 24 | + * |
| 25 | + * Each supported annotation renderer must have an open and close property, each either a string or |
| 26 | + * a function which accepts a data argument. |
| 27 | + * |
| 28 | + * @static |
| 29 | + * @member |
| 30 | + */ |
| 31 | +es.ContentView.annotationRenderers = { |
| 32 | + 'object/template': { |
| 33 | + 'open': function( data ) { |
| 34 | + return '<span class="es-contentView-format-object" contentEditable="false">' + data.html; |
| 35 | + }, |
| 36 | + 'close': '</span>' |
| 37 | + }, |
| 38 | + 'object/hook': { |
| 39 | + 'open': function( data ) { |
| 40 | + return '<span class="es-contentView-format-object">' + data.html; |
| 41 | + }, |
| 42 | + 'close': '</span>' |
| 43 | + }, |
| 44 | + 'textStyle/bold': { |
| 45 | + //'open': '<span class="es-contentView-format-textStyle-bold">', |
| 46 | + //'close': '</span>' |
| 47 | + 'open': '<b>', |
| 48 | + 'close': '</b>' |
| 49 | + }, |
| 50 | + 'textStyle/italic': { |
| 51 | + 'open': '<span class="es-contentView-format-textStyle-italic">', |
| 52 | + 'close': '</span>' |
| 53 | + }, |
| 54 | + 'textStyle/strong': { |
| 55 | + 'open': '<span class="es-contentView-format-textStyle-strong">', |
| 56 | + 'close': '</span>' |
| 57 | + }, |
| 58 | + 'textStyle/emphasize': { |
| 59 | + 'open': '<span class="es-contentView-format-textStyle-emphasize">', |
| 60 | + 'close': '</span>' |
| 61 | + }, |
| 62 | + 'textStyle/big': { |
| 63 | + 'open': '<span class="es-contentView-format-textStyle-big">', |
| 64 | + 'close': '</span>' |
| 65 | + }, |
| 66 | + 'textStyle/small': { |
| 67 | + 'open': '<span class="es-contentView-format-textStyle-small">', |
| 68 | + 'close': '</span>' |
| 69 | + }, |
| 70 | + 'textStyle/superScript': { |
| 71 | + 'open': '<span class="es-contentView-format-textStyle-superScript">', |
| 72 | + 'close': '</span>' |
| 73 | + }, |
| 74 | + 'textStyle/subScript': { |
| 75 | + 'open': '<span class="es-contentView-format-textStyle-subScript">', |
| 76 | + 'close': '</span>' |
| 77 | + }, |
| 78 | + 'link/external': { |
| 79 | + 'open': function( data ) { |
| 80 | + return '<span class="es-contentView-format-link" data-href="' + data.href + '">'; |
| 81 | + }, |
| 82 | + 'close': '</span>' |
| 83 | + }, |
| 84 | + 'link/internal': { |
| 85 | + 'open': function( data ) { |
| 86 | + return '<span class="es-contentView-format-link" data-title="wiki/' + data.title + '">'; |
| 87 | + }, |
| 88 | + 'close': '</span>' |
| 89 | + } |
| 90 | +}; |
| 91 | + |
| 92 | +/** |
| 93 | + * Mapping of character and HTML entities or renderings. |
| 94 | + * |
| 95 | + * @static |
| 96 | + * @member |
| 97 | + */ |
| 98 | +es.ContentView.htmlCharacters = { |
| 99 | + '&': '&', |
| 100 | + '<': '<', |
| 101 | + '>': '>', |
| 102 | + '\'': ''', |
| 103 | + '"': '"', |
| 104 | + '\n': '<span class="es-contentView-whitespace">¶</span>', |
| 105 | + '\t': '<span class="es-contentView-whitespace">⇾</span>', |
| 106 | + ' ': ' ' |
| 107 | +}; |
| 108 | + |
| 109 | +/* Static Methods */ |
| 110 | + |
| 111 | +/** |
| 112 | + * Gets a rendered opening or closing of an annotation. |
| 113 | + * |
| 114 | + * Tag nesting is handled using a stack, which keeps track of what is currently open. A common stack |
| 115 | + * argument should be used while rendering content. |
| 116 | + * |
| 117 | + * @static |
| 118 | + * @method |
| 119 | + * @param {String} bias Which side of the annotation to render, either "open" or "close" |
| 120 | + * @param {Object} annotation Annotation to render |
| 121 | + * @param {Array} stack List of currently open annotations |
| 122 | + * @returns {String} Rendered annotation |
| 123 | + */ |
| 124 | +es.ContentView.renderAnnotation = function( bias, annotation, stack ) { |
| 125 | + var renderers = es.ContentView.annotationRenderers, |
| 126 | + type = annotation.type, |
| 127 | + out = ''; |
| 128 | + if ( type in renderers ) { |
| 129 | + if ( bias === 'open' ) { |
| 130 | + // Add annotation to the top of the stack |
| 131 | + stack.push( annotation ); |
| 132 | + // Open annotation |
| 133 | + out += typeof renderers[type].open === 'function' ? |
| 134 | + renderers[type].open( annotation.data ) : renderers[type].open; |
| 135 | + } else { |
| 136 | + if ( stack[stack.length - 1] === annotation ) { |
| 137 | + // Remove annotation from top of the stack |
| 138 | + stack.pop(); |
| 139 | + // Close annotation |
| 140 | + out += typeof renderers[type].close === 'function' ? |
| 141 | + renderers[type].close( annotation.data ) : renderers[type].close; |
| 142 | + } else { |
| 143 | + // Find the annotation in the stack |
| 144 | + var depth = es.inArray( annotation, stack ), |
| 145 | + i; |
| 146 | + if ( depth === -1 ) { |
| 147 | + throw 'Invalid stack error. An element is missing from the stack.'; |
| 148 | + } |
| 149 | + // Close each already opened annotation |
| 150 | + for ( i = stack.length - 1; i >= depth + 1; i-- ) { |
| 151 | + out += typeof renderers[stack[i].type].close === 'function' ? |
| 152 | + renderers[stack[i].type].close( stack[i].data ) : |
| 153 | + renderers[stack[i].type].close; |
| 154 | + } |
| 155 | + // Close the buried annotation |
| 156 | + out += typeof renderers[type].close === 'function' ? |
| 157 | + renderers[type].close( annotation.data ) : renderers[type].close; |
| 158 | + // Re-open each previously opened annotation |
| 159 | + for ( i = depth + 1; i < stack.length; i++ ) { |
| 160 | + out += typeof renderers[stack[i].type].open === 'function' ? |
| 161 | + renderers[stack[i].type].open( stack[i].data ) : |
| 162 | + renderers[stack[i].type].open; |
| 163 | + } |
| 164 | + // Remove the annotation from the middle of the stack |
| 165 | + stack.splice( depth, 1 ); |
| 166 | + } |
| 167 | + } |
| 168 | + } |
| 169 | + return out; |
| 170 | +}; |
| 171 | + |
| 172 | +/* Methods */ |
| 173 | + |
| 174 | +es.ContentView.prototype.render = function( offset ) { |
| 175 | + this.$.html(this.getHtml(0, this.model.getContentLength())); |
| 176 | +}; |
| 177 | + |
| 178 | +/** |
| 179 | + * Gets an HTML rendering of a range of data within content model. |
| 180 | + * |
| 181 | + * @method |
| 182 | + * @param {es.Range} range Range of content to render |
| 183 | + * @param {String} Rendered HTML of data within content model |
| 184 | + */ |
| 185 | +es.ContentView.prototype.getHtml = function( range, options ) { |
| 186 | + if ( range ) { |
| 187 | + range.normalize(); |
| 188 | + } else { |
| 189 | + range = { 'start': 0, 'end': undefined }; |
| 190 | + } |
| 191 | + var data = this.model.getContentData(), |
| 192 | + render = es.ContentView.renderAnnotation, |
| 193 | + htmlChars = es.ContentView.htmlCharacters; |
| 194 | + var out = '', |
| 195 | + left = '', |
| 196 | + right, |
| 197 | + leftPlain, |
| 198 | + rightPlain, |
| 199 | + stack = [], |
| 200 | + chr, |
| 201 | + i, |
| 202 | + j; |
| 203 | + for ( i = 0; i < data.length; i++ ) { |
| 204 | + right = data[i]; |
| 205 | + leftPlain = typeof left === 'string'; |
| 206 | + rightPlain = typeof right === 'string'; |
| 207 | + if ( !leftPlain && rightPlain ) { |
| 208 | + // [formatted][plain] pair, close any annotations for left |
| 209 | + for ( j = 1; j < left.length; j++ ) { |
| 210 | + out += render( 'close', left[j], stack ); |
| 211 | + } |
| 212 | + } else if ( leftPlain && !rightPlain ) { |
| 213 | + // [plain][formatted] pair, open any annotations for right |
| 214 | + for ( j = 1; j < right.length; j++ ) { |
| 215 | + out += render( 'open', right[j], stack ); |
| 216 | + } |
| 217 | + } else if ( !leftPlain && !rightPlain ) { |
| 218 | + // [formatted][formatted] pair, open/close any differences |
| 219 | + for ( j = 1; j < left.length; j++ ) { |
| 220 | + if ( es.inArray( left[j], right ) === -1 ) { |
| 221 | + out += render( 'close', left[j], stack ); |
| 222 | + } |
| 223 | + } |
| 224 | + for ( j = 1; j < right.length; j++ ) { |
| 225 | + if ( es.inArray( right[j], left ) === -1 ) { |
| 226 | + out += render( 'open', right[j], stack ); |
| 227 | + } |
| 228 | + } |
| 229 | + } |
| 230 | + chr = rightPlain ? right : right[0]; |
| 231 | + out += chr in htmlChars ? htmlChars[chr] : chr; |
| 232 | + left = right; |
| 233 | + } |
| 234 | + // Close all remaining tags at the end of the content |
| 235 | + if ( !rightPlain && right ) { |
| 236 | + for ( j = 1; j < right.length; j++ ) { |
| 237 | + out += render( 'close', right[j], stack ); |
| 238 | + } |
| 239 | + } |
| 240 | + return out; |
| 241 | +}; |
| 242 | + |
| 243 | +/* Inheritance */ |
| 244 | + |
| 245 | +es.extendClass( es.ContentView, es.EventEmitter ); |
Property changes on: trunk/extensions/VisualEditor/contentEditable/views/es.ContentView.js |
___________________________________________________________________ |
Added: svn:eol-style |
246 | 246 | + native |
Index: trunk/extensions/VisualEditor/contentEditable/views/es.DocumentViewLeafNode.js |
— | — | @@ -1,91 +1,91 @@ |
2 | | -/**
|
3 | | - * Creates an es.DocumentViewLeafNode object.
|
4 | | - *
|
5 | | - * @class
|
6 | | - * @abstract
|
7 | | - * @constructor
|
8 | | - * @extends {es.DocumentLeafNode}
|
9 | | - * @extends {es.DocumentViewNode}
|
10 | | - * @param model {es.ModelNode} Model to observe
|
11 | | - * @param {jQuery} [$element] Element to use as a container
|
12 | | - */
|
13 | | -es.DocumentViewLeafNode = function( model, $element ) {
|
14 | | - // Inheritance
|
15 | | - es.DocumentLeafNode.call( this );
|
16 | | - es.DocumentViewNode.call( this, model, $element );
|
17 | | -
|
18 | | - this.$.data('view', this);
|
19 | | -
|
20 | | - // Properties
|
21 | | - this.$content = this.$;
|
22 | | - this.contentView = new es.ContentView( this.$content, model );
|
23 | | -
|
24 | | - // Events
|
25 | | - this.contentView.on( 'update', this.emitUpdate );
|
26 | | -};
|
27 | | -
|
28 | | -/* Methods */
|
29 | | -
|
30 | | -/**
|
31 | | - * Render content.
|
32 | | - *
|
33 | | - * @method
|
34 | | - */
|
35 | | -es.DocumentViewLeafNode.prototype.renderContent = function() {
|
36 | | - this.contentView.render();
|
37 | | -};
|
38 | | -
|
39 | | -/**
|
40 | | - * Draw selection around a given range.
|
41 | | - *
|
42 | | - * @method
|
43 | | - * @param {es.Range} range Range of content to draw selection around
|
44 | | - */
|
45 | | -es.DocumentViewLeafNode.prototype.drawSelection = function( range ) {
|
46 | | - this.contentView.drawSelection( range );
|
47 | | -};
|
48 | | -
|
49 | | -/**
|
50 | | - * Clear selection.
|
51 | | - *
|
52 | | - * @method
|
53 | | - */
|
54 | | -es.DocumentViewLeafNode.prototype.clearSelection = function() {
|
55 | | - this.contentView.clearSelection();
|
56 | | -};
|
57 | | -
|
58 | | -/**
|
59 | | - * Gets the nearest offset of a rendered position.
|
60 | | - *
|
61 | | - * @method
|
62 | | - * @param {es.Position} position Position to get offset for
|
63 | | - * @returns {Integer} Offset of position
|
64 | | - */
|
65 | | -es.DocumentViewLeafNode.prototype.getOffsetFromRenderedPosition = function( position ) {
|
66 | | - return this.contentView.getOffsetFromRenderedPosition( position );
|
67 | | -};
|
68 | | -
|
69 | | -/**
|
70 | | - * Gets rendered position of offset within content.
|
71 | | - *
|
72 | | - * @method
|
73 | | - * @param {Integer} offset Offset to get position for
|
74 | | - * @returns {es.Position} Position of offset
|
75 | | - */
|
76 | | -es.DocumentViewLeafNode.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) {
|
77 | | - var position = this.contentView.getRenderedPositionFromOffset( offset, leftBias ),
|
78 | | - contentPosition = this.$content.offset();
|
79 | | - position.top += contentPosition.top;
|
80 | | - position.left += contentPosition.left;
|
81 | | - position.bottom += contentPosition.top;
|
82 | | - return position;
|
83 | | -};
|
84 | | -
|
85 | | -es.DocumentViewLeafNode.prototype.getRenderedLineRangeFromOffset = function( offset ) {
|
86 | | - return this.contentView.getRenderedLineRangeFromOffset( offset );
|
87 | | -};
|
88 | | -
|
89 | | -/* Inheritance */
|
90 | | -
|
91 | | -es.extendClass( es.DocumentViewLeafNode, es.DocumentLeafNode );
|
92 | | -es.extendClass( es.DocumentViewLeafNode, es.DocumentViewNode );
|
| 2 | +/** |
| 3 | + * Creates an es.DocumentViewLeafNode object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @abstract |
| 7 | + * @constructor |
| 8 | + * @extends {es.DocumentLeafNode} |
| 9 | + * @extends {es.DocumentViewNode} |
| 10 | + * @param model {es.ModelNode} Model to observe |
| 11 | + * @param {jQuery} [$element] Element to use as a container |
| 12 | + */ |
| 13 | +es.DocumentViewLeafNode = function( model, $element ) { |
| 14 | + // Inheritance |
| 15 | + es.DocumentLeafNode.call( this ); |
| 16 | + es.DocumentViewNode.call( this, model, $element ); |
| 17 | + |
| 18 | + this.$.data('view', this); |
| 19 | + |
| 20 | + // Properties |
| 21 | + this.$content = this.$; |
| 22 | + this.contentView = new es.ContentView( this.$content, model ); |
| 23 | + |
| 24 | + // Events |
| 25 | + this.contentView.on( 'update', this.emitUpdate ); |
| 26 | +}; |
| 27 | + |
| 28 | +/* Methods */ |
| 29 | + |
| 30 | +/** |
| 31 | + * Render content. |
| 32 | + * |
| 33 | + * @method |
| 34 | + */ |
| 35 | +es.DocumentViewLeafNode.prototype.renderContent = function() { |
| 36 | + this.contentView.render(); |
| 37 | +}; |
| 38 | + |
| 39 | +/** |
| 40 | + * Draw selection around a given range. |
| 41 | + * |
| 42 | + * @method |
| 43 | + * @param {es.Range} range Range of content to draw selection around |
| 44 | + */ |
| 45 | +es.DocumentViewLeafNode.prototype.drawSelection = function( range ) { |
| 46 | + this.contentView.drawSelection( range ); |
| 47 | +}; |
| 48 | + |
| 49 | +/** |
| 50 | + * Clear selection. |
| 51 | + * |
| 52 | + * @method |
| 53 | + */ |
| 54 | +es.DocumentViewLeafNode.prototype.clearSelection = function() { |
| 55 | + this.contentView.clearSelection(); |
| 56 | +}; |
| 57 | + |
| 58 | +/** |
| 59 | + * Gets the nearest offset of a rendered position. |
| 60 | + * |
| 61 | + * @method |
| 62 | + * @param {es.Position} position Position to get offset for |
| 63 | + * @returns {Integer} Offset of position |
| 64 | + */ |
| 65 | +es.DocumentViewLeafNode.prototype.getOffsetFromRenderedPosition = function( position ) { |
| 66 | + return this.contentView.getOffsetFromRenderedPosition( position ); |
| 67 | +}; |
| 68 | + |
| 69 | +/** |
| 70 | + * Gets rendered position of offset within content. |
| 71 | + * |
| 72 | + * @method |
| 73 | + * @param {Integer} offset Offset to get position for |
| 74 | + * @returns {es.Position} Position of offset |
| 75 | + */ |
| 76 | +es.DocumentViewLeafNode.prototype.getRenderedPositionFromOffset = function( offset, leftBias ) { |
| 77 | + var position = this.contentView.getRenderedPositionFromOffset( offset, leftBias ), |
| 78 | + contentPosition = this.$content.offset(); |
| 79 | + position.top += contentPosition.top; |
| 80 | + position.left += contentPosition.left; |
| 81 | + position.bottom += contentPosition.top; |
| 82 | + return position; |
| 83 | +}; |
| 84 | + |
| 85 | +es.DocumentViewLeafNode.prototype.getRenderedLineRangeFromOffset = function( offset ) { |
| 86 | + return this.contentView.getRenderedLineRangeFromOffset( offset ); |
| 87 | +}; |
| 88 | + |
| 89 | +/* Inheritance */ |
| 90 | + |
| 91 | +es.extendClass( es.DocumentViewLeafNode, es.DocumentLeafNode ); |
| 92 | +es.extendClass( es.DocumentViewLeafNode, es.DocumentViewNode ); |
Property changes on: trunk/extensions/VisualEditor/contentEditable/views/es.DocumentViewLeafNode.js |
___________________________________________________________________ |
Added: svn:eol-style |
93 | 93 | + native |
Index: trunk/extensions/VisualEditor/contentEditable/views/es.DocumentView.js |
— | — | @@ -1,72 +1,72 @@ |
2 | | -/**
|
3 | | - * Creates an es.DocumentView object.
|
4 | | - *
|
5 | | - * @class
|
6 | | - * @constructor
|
7 | | - * @extends {es.DocumentViewBranchNode}
|
8 | | - * @param {es.DocumentModel} documentModel Document model to view
|
9 | | - * @param {es.SurfaceView} surfaceView Surface view this view is a child of
|
10 | | - */
|
11 | | -es.DocumentView = function( model, surfaceView ) {
|
12 | | - // Inheritance
|
13 | | - es.DocumentViewBranchNode.call( this, model );
|
14 | | -
|
15 | | - // Properties
|
16 | | - this.surfaceView = surfaceView;
|
17 | | -
|
18 | | - // DOM Changes
|
19 | | - this.$.addClass( 'es-documentView' );
|
20 | | - this.$.attr('contentEditable', 'true');
|
21 | | -};
|
22 | | -
|
23 | | -/* Static Members */
|
24 | | -
|
25 | | -
|
26 | | -/**
|
27 | | - * Mapping of symbolic names and splitting rules.
|
28 | | - *
|
29 | | - * Each rule is an object with a self and children property. Each of these properties may contain
|
30 | | - * one of two possible values:
|
31 | | - * Boolean - Whether a split is allowed
|
32 | | - * Null - Node is a leaf, so there's nothing to split
|
33 | | - *
|
34 | | - * @example Paragraph rules
|
35 | | - * {
|
36 | | - * 'self': true
|
37 | | - * 'children': null
|
38 | | - * }
|
39 | | - * @example List rules
|
40 | | - * {
|
41 | | - * 'self': false,
|
42 | | - * 'children': true
|
43 | | - * }
|
44 | | - * @example ListItem rules
|
45 | | - * {
|
46 | | - * 'self': true,
|
47 | | - * 'children': false
|
48 | | - * }
|
49 | | - */
|
50 | | -es.DocumentView.splitRules = {};
|
51 | | -
|
52 | | -/* Methods */
|
53 | | -
|
54 | | -/**
|
55 | | - * Get the document offset of a position created from passed DOM event
|
56 | | - *
|
57 | | - * @method
|
58 | | - * @param e {Event} Event to create es.Position from
|
59 | | - * @returns {Integer} Document offset
|
60 | | - */
|
61 | | -es.DocumentView.prototype.getOffsetFromEvent = function( e ) {
|
62 | | - var position = es.Position.newFromEventPagePosition( e );
|
63 | | - return this.getOffsetFromRenderedPosition( position );
|
64 | | -};
|
65 | | -
|
66 | | -es.DocumentView.splitRules.document = {
|
67 | | - 'self': false,
|
68 | | - 'children': true
|
69 | | -};
|
70 | | -
|
71 | | -/* Inheritance */
|
72 | | -
|
73 | | -es.extendClass( es.DocumentView, es.DocumentViewBranchNode );
|
| 2 | +/** |
| 3 | + * Creates an es.DocumentView object. |
| 4 | + * |
| 5 | + * @class |
| 6 | + * @constructor |
| 7 | + * @extends {es.DocumentViewBranchNode} |
| 8 | + * @param {es.DocumentModel} documentModel Document model to view |
| 9 | + * @param {es.SurfaceView} surfaceView Surface view this view is a child of |
| 10 | + */ |
| 11 | +es.DocumentView = function( model, surfaceView ) { |
| 12 | + // Inheritance |
| 13 | + es.DocumentViewBranchNode.call( this, model ); |
| 14 | + |
| 15 | + // Properties |
| 16 | + this.surfaceView = surfaceView; |
| 17 | + |
| 18 | + // DOM Changes |
| 19 | + this.$.addClass( 'es-documentView' ); |
| 20 | + this.$.attr('contentEditable', 'true'); |
| 21 | +}; |
| 22 | + |
| 23 | +/* Static Members */ |
| 24 | + |
| 25 | + |
| 26 | +/** |
| 27 | + * Mapping of symbolic names and splitting rules. |
| 28 | + * |
| 29 | + * Each rule is an object with a self and children property. Each of these properties may contain |
| 30 | + * one of two possible values: |
| 31 | + * Boolean - Whether a split is allowed |
| 32 | + * Null - Node is a leaf, so there's nothing to split |
| 33 | + * |
| 34 | + * @example Paragraph rules |
| 35 | + * { |
| 36 | + * 'self': true |
| 37 | + * 'children': null |
| 38 | + * } |
| 39 | + * @example List rules |
| 40 | + * { |
| 41 | + * 'self': false, |
| 42 | + * 'children': true |
| 43 | + * } |
| 44 | + * @example ListItem rules |
| 45 | + * { |
| 46 | + * 'self': true, |
| 47 | + * 'children': false |
| 48 | + * } |
| 49 | + */ |
| 50 | +es.DocumentView.splitRules = {}; |
| 51 | + |
| 52 | +/* Methods */ |
| 53 | + |
| 54 | +/** |
| 55 | + * Get the document offset of a position created from passed DOM event |
| 56 | + * |
| 57 | + * @method |
| 58 | + * @param e {Event} Event to create es.Position from |
| 59 | + * @returns {Integer} Document offset |
| 60 | + */ |
| 61 | +es.DocumentView.prototype.getOffsetFromEvent = function( e ) { |
| 62 | + var position = es.Position.newFromEventPagePosition( e ); |
| 63 | + return this.getOffsetFromRenderedPosition( position ); |
| 64 | +}; |
| 65 | + |
| 66 | +es.DocumentView.splitRules.document = { |
| 67 | + 'self': false, |
| 68 | + 'children': true |
| 69 | +}; |
| 70 | + |
| 71 | +/* Inheritance */ |
| 72 | + |
| 73 | +es.extendClass( es.DocumentView, es.DocumentViewBranchNode ); |
Property changes on: trunk/extensions/VisualEditor/contentEditable/views/es.DocumentView.js |
___________________________________________________________________ |
Added: svn:eol-style |
74 | 74 | + native |
Property changes on: trunk/extensions/VisualEditor/contentEditable/diff_match_patch.js |
___________________________________________________________________ |
Added: svn:eol-style |
75 | 75 | + native |
Property changes on: trunk/extensions/MwEmbedSupport/MwEmbedModules/MwEmbedSupport/jquery.loadingSpinner/spinner.js |
___________________________________________________________________ |
Added: svn:eol-style |
76 | 76 | + native |
Property changes on: trunk/extensions/EducationProgram/specials/SpecialInstitutionHistory.php |
___________________________________________________________________ |
Added: svn:eol-style |
77 | 77 | + native |
Property changes on: trunk/extensions/EducationProgram/specials/SpecialCourseHistory.php |
___________________________________________________________________ |
Added: svn:eol-style |
78 | 78 | + native |
Property changes on: trunk/extensions/EducationProgram/specials/SpecialEPHistory.php |
___________________________________________________________________ |
Added: svn:eol-style |
79 | 79 | + native |
Property changes on: trunk/extensions/EducationProgram/includes/EPRevisionPager.php |
___________________________________________________________________ |
Added: svn:eol-style |
80 | 80 | + native |
Property changes on: trunk/extensions/EducationProgram/includes/EPRevisions.php |
___________________________________________________________________ |
Added: svn:eol-style |
81 | 81 | + native |
Property changes on: trunk/extensions/EducationProgram/includes/EPRevision.php |
___________________________________________________________________ |
Added: svn:eol-style |
82 | 82 | + native |
Property changes on: trunk/extensions/EducationProgram/includes/EPPageObject.php |
___________________________________________________________________ |
Added: svn:eol-style |
83 | 83 | + native |