source: gs3-extensions/seaweed-debug/trunk/src/Doc.js@ 25160

Last change on this file since 25160 was 25160, checked in by sjm84, 12 years ago

Initial cut at a version of seaweed for debugging purposes. Check it out live into the web/ext folder

File size: 18.9 KB
Line 
1/*
2 * file: Doc.js
3 *
4 * @BEGINLICENSE
5 * Copyright 2010 Brook Novak (email : [email protected])
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 * @ENDLICENSE
18 */
19bootstrap.provides("Doc");
20
21
22var
23 /**
24 * @final
25 * The protected node classname prefix
26 * @type String
27 */
28 _PROTECTED_CLASS = "sw-protect",
29
30 /**
31 * @final
32 * The editable section node classname prefix
33 * @type String
34 */
35 _ES_CLASS_PREFIX = "editable",
36
37 /**
38 * @final
39 * The classname use for packaged nodes
40 * @type String
41 */
42 _PACKAGE_CLASS_NAME = "sw-packaged";
43
44
45/**
46 * Note: may want to check _doesNeedESPlaceholder first.
47 *
48 * @param {Node} domNode A dom node in the document to test.
49 * @return {Boolean} True if the given dom node is in need of a modifiable placeholder.
50 *
51 * @see _doesNeedESPlaceholder
52 */
53function _doesNeedMNPlaceholder(domNode){
54 if (_isPlaceholderCandidate(domNode))
55 return !_doesContainTanglableDescendant(domNode);
56 return false;
57}
58
59/**
60 * @param {Node} domNode A dom node in the document to test.
61 * @return {Boolean} True if the given dom node is in need of a editable section placeholder.
62 *
63 * @see _doesNeedMNPlaceholder
64 */
65function _doesNeedESPlaceholder(domNode){
66 if (de.doc.isEditSection(domNode))
67 return !_doesContainTanglableDescendant(domNode);
68 return false;
69}
70
71
72/**
73 * @param {Node} domNode A dom node to test
74 * @return {Boolean} True iff the dom node contains a descendant for which the cursor can be placed by.
75 */
76function _doesContainTanglableDescendant(domNode) {
77
78 var containsTangableNode = false;
79
80 // Check if needs a placeholder
81 _visitAllNodes(domNode, domNode, true, function(node){
82
83 if (node == domNode || de.doc.isProtectedNode(node)) return;
84
85 var pflags = de.cursor.getPlacementFlags(node);
86 if (pflags == de.cursor.PlacementFlag.INSIDE) {
87
88 if (_doesTextSupportNonWS(node) && _nodeLength(node) > 0) {
89
90 if (_isAllWhiteSpace(node.nodeValue)) {
91
92 // Validate that the text node is tangable
93 var measureSpan = $createElement("span"),
94 measureText = document.createTextNode(node.nodeValue);
95
96 measureSpan.appendChild(measureText);
97 node.nodeValue = "";
98 node.parentNode.appendChild(measureSpan);
99 containsTangableNode = measureSpan.offsetHeight != 0 && measureSpan.offsetWidth != 0;
100 node.parentNode.removeChild(measureSpan);
101 node.nodeValue = measureText.nodeValue;
102
103 // TODO: Opera sometimes (randomly) incorrectly sets the offset width/height to zero
104 // on text nodes...
105
106 } else containsTangableNode = true;
107 }
108 } else if (pflags) containsTangableNode = true;
109
110 return !containsTangableNode;
111 });
112
113 return containsTangableNode;
114} // End function doesContainTanglableDescendant
115
116
117/**
118 *
119 * @param {Node} node A dom node to test
120 * @return {Undefined, Boolean} True if node is a placeholder candidate. Undefined if it is not.
121 */
122var _isPlaceholderCandidate = function(){
123 /*
124 * A map containing elements the cursor can navigate into which do not contain a "Tangle node."
125 * Excludes body
126 */
127 var placeholderCandidates = $createLookupMap("li,dd,dt,p,td,th,h1,h2,h3,h4,h5,h6,pre,div");
128
129 return function(node){
130 return placeholderCandidates[_nodeName(node)] || node == docBody;
131 }
132}();
133
134
135
136(function(){
137
138 $enqueueInit("Doc", function() {
139
140 // Make as subject
141 _model(de.doc);
142
143 // Preprocess the document: consolidate all existing/initial editable sections
144 var es = de.doc.getAllEditSections();
145
146 _recordOperations = false;
147 for (var i in es) {
148 _consolidateWSSeqs(es[i], true);
149 }
150 _recordOperations = true;
151
152 // For dynamically added editable sections, consolidate them too.
153 de.doc.addObserver({
154 onSectionAdded : function(editSection) {
155 // Only consolidate within the editable sections to avoid possibilities of consoldiating
156 // surrounding editable DOM with Undo/Redo history.
157 _recordOperations = false;
158 _consolidateWSSeqs(editSection, false);
159 _recordOperations = true;
160 }
161 });
162
163 });
164
165 var
166 propertySetMap = {},
167 MN_PH_CLASS = "sw-mn-ph",
168 ES_PH_CLASS = "sw-es-ph",
169 ES_CLASS_TEST_REGEXP = new RegExp("^" + _ES_CLASS_PREFIX + ".*$"),
170 ES_CLASS_MATCH_REGEXP = new RegExp("^" + _ES_CLASS_PREFIX + "-?(.+)$"),
171 PROTECTED_NODE_TEST_REGEXP = new RegExp("^" + _PROTECTED_CLASS + "$"),
172 PACKAGED_NODE_TEST_REGEXP = new RegExp("^" + _PACKAGE_CLASS_NAME + "$");
173
174 // Create the doc namespace
175
176 /**
177 * @namespace
178 * Provides CSS like language for declaring and customizing editable sections on web pages.
179 *
180 * <br><br>
181 * Whenever a new editable section has been dynamically added to the document, a "onSectionAdded" event is fired,
182 * where the argument is the added editable section.
183 *
184 * <br><br>
185 * Whenever a editable section has been dynamically removed from the document, a "onSectionRemoved" event is fired,
186 * where the argument is the removed editable section.
187 *
188 * @borrows de.mvc.AbstractSubject#addObserver as this.addObserver
189 *
190 * @borrows de.mvc.AbstractSubject#removeObserver as this.removeObserver
191 *
192 * @author Brook Novak
193 */
194 de.doc = {
195
196 /**
197 * @param {Node} node A dom node
198 *
199 * @return {Node} The first ancestor element of node which is an editable section, inclusive of the given node itself.
200 * Null if the node / none of its ancestors are editable sections.
201 */
202 getEditSectionContainer: function(node){
203 return _findAncestor(node, null, this.isEditSection, true);
204 },
205
206 /**
207 * Determines whether a dom node is marked as edit section element.
208 *
209 * @param {Node} node The dom node to test
210 *
211 * @return {Boolean} True if node is a edit section element
212 */
213 isEditSection: function(node){
214 if (node && node.nodeType == Node.ELEMENT_NODE)
215 return _findClassName(node, ES_CLASS_TEST_REGEXP);
216 return false;
217 },
218
219 /**
220 * @return {[Node]} An array of editable sections currently in the document.
221 */
222 getAllEditSections : function() {
223
224 var editSections = [];
225 _visitAllNodes(docBody, docBody, true, function(domNode) {
226 if (de.doc.isEditSection(domNode)) editSections.push(domNode);
227 });
228 return editSections;
229
230 },
231
232 /**
233 * @param {Node} domNode A dom node to test
234 * @return {Boolean} True if the given dom node is a descendant of an editable section.
235 */
236 isNodeEditable : function(domNode) {
237 var es = this.getEditSectionContainer(domNode);
238 return es != null && es != domNode;
239 },
240
241 /**
242 * @param {Node} domNode A dom node to test
243 * @return {Node} The protected nodes container is the node is proected. If the dom is a container then
244 * it will be returned. Otherwise null will be returned.
245 */
246 getProtectedNodeContainer : function(domNode) {
247 return _findAncestor(domNode, null, function(node) {
248 return node.nodeType == Node.ELEMENT_NODE && _findClassName(node, PROTECTED_NODE_TEST_REGEXP);
249 }, true);
250 },
251
252 /**
253 * @param {Node} domNode A dom node to test
254 * @return {Boolean} True if the given dom node is, or is an descendant of, a protected node
255 */
256 isProtectedNode : function(domNode) {
257 return this.getProtectedNodeContainer(domNode) != null;
258 },
259
260 /**
261 * @param {Node} domNode A dom node to test
262 *
263 * @return {Node} The package root node. Null if the node is not packaged.
264 */
265 getPackageContainer : function(domNode) {
266 return _findAncestor(domNode, null, function(node) {
267 return node.nodeType == Node.ELEMENT_NODE && _findClassName(node, PACKAGED_NODE_TEST_REGEXP);
268 }, true);
269 },
270
271 /**
272 * A "packaged" node is part of a tree of nodes which are not allowed to be edited, although
273 * they are in an editable section.
274 *
275 * @param {Node} domNode A dom node to test
276 * @return {Boolean} True if the given dom node is part of a package.
277 */
278 isNodePackaged : function(domNode) {
279 return this.getPackageContainer(domNode) != null;
280 },
281
282 /**
283 * Declares or overrides a property set for editable sections.
284 * <br><br>
285 * Attributes:
286 *
287 * actionFilter: "[!][actionname1[,actionname2[,...]]]"
288 * where actual name is case insensitive undoable action name.
289 * Format action can be followed by sub-action encapsulated in brackets
290 * <br><br>
291 * If the property name exists, then the existing set will be overridden.
292 * You can override the default property set by using the name "defaultSet".
293 *
294 * @example
295 * de.doc.declarePropertySet("metadata", {
296 * inputFilter: "[[a-z][A-Z][1-9]\\s\\n]*",
297 * actionFilter: "!blockquote,changecontainer,format(link)"
298 * Accept all actions EXCEPT for blockquote,changecontainer,formatting links
299 *
300 * });
301 *
302 * @see TODO REFER TO SPEC
303 *
304 * @param {String} name
305 * The name of the class being declared.
306 *
307 *
308 * @param {Object} properties
309 * A set of attributes that is associated with the given name.
310 *
311 */
312 declarePropertySet: function(name, properties) {
313 properties = _clone(properties);
314
315 // Build action filter
316 if (typeof properties.actionFilter == "string") {
317
318 var actionFilter = properties.actionFilter;
319
320 // Is the action inclusive or exclusive?
321 if (actionFilter.charAt(0) != '!') {
322 properties.afInclusive = true;
323 // Add implicit text editing actions
324 if (actionFilter)
325 actionFilter += ",";
326 actionFilter += "inserthtml,inserttext,removedom,removetext";
327 } else {
328 actionFilter = actionFilter.substr(1);
329 properties.afInclusive = false;
330 }
331
332 // Break filter into tokens
333 var tokens = actionFilter.toLowerCase().split(',');
334
335 // Build up the reg exp for quick filtering
336 var reStr = "(";
337
338 for (var i in tokens) {
339
340 reStr += ((i=='0') ? "" : "|");
341
342 var token = tokens[i];
343
344 var match = /^format\((.+)\)$/.exec(token);
345
346 // Format actions can have sub-action filters
347 if (match) {
348
349 var subTokens = match[1].split(',');
350
351 for (var j in subTokens) {
352 reStr += ((j == '0') ? "" : "|") + "format" + subTokens[j];
353 }
354
355 } else {
356
357 // Add the action name to the reg exp set
358 reStr += token;
359
360 if (token.indexOf("format") == 0)
361 reStr += ".+"; // If not sub-format actions are defined then declare as all format actions
362 }
363
364 } // End loop: parsing action tokens
365
366 reStr += ")";
367
368 properties.afRE = new RegExp("^" + reStr + "$");
369 }
370
371 // Set or override a property set
372 propertySetMap[name] = properties;
373
374
375 },
376
377 /**
378 * Declares a batch of property sets in a single call
379 * @param {Object} sets An object containing key-value pairs, when the keys are property set names,
380 * and values are objects containing properties.
381 */
382 declarePropertySets: function(sets) {
383 for (var tuple in sets) {
384 this.declarePropertySet(tuple, sets[tuple]);
385 }
386 },
387
388 /**
389 * Retreives the editable property set for a given node. This considers property inheritance
390 *
391 * @param {Node} node A dom node
392 *
393 * @return {Object} A set of read only dedit atrributes that the given node has inherited.
394 * Null if the node is not a (or descendant of a) editable section.
395 */
396 getEditProperties : function (node) {
397
398 // Get the editable section container for this node
399 if (!this.isEditSection(node))
400 node = this.getEditSectionContainer(node);
401
402 if (node) {
403
404 // Get the property set name for this node
405 var esClassName = _findClassName(node, ES_CLASS_TEST_REGEXP);
406
407 if (esClassName) {
408 var nameMatch = ES_CLASS_MATCH_REGEXP.exec(esClassName);
409 return nameMatch ? propertySetMap[nameMatch[1]] || {} : {};
410 }
411
412 return {};
413
414 }
415
416 return null;
417 },
418
419 /**
420 * @return {Node} A modifiable node placeholder element.
421 *
422 * @see The DOM-based Web Editor Specification 1.0: Section 1.4
423 */
424 createMNPlaceholder: function(){
425 var ph = $createElement("span");
426 _setClassName(ph, MN_PH_CLASS);
427 ph.innerHTML = "&nbsp;";
428 return ph;
429 },
430
431 /**
432 * Determines whether a dom node is (part of) a modifiable node placeholder.
433 *
434 * @param {Node} node A dom node to test
435 *
436 * @param {Boolean} immediate True to only test if node is a placeholder.
437 * False to also test if nodes parent is a placeholder.
438 *
439 * @return {Boolean} True if node is (part of) a modifiable node placeholder. False if it is not
440 *
441 * @see The DOM-based Web Editor Specification 1.0: Section 1.4
442 */
443 isMNPlaceHolder: function(node, immediate){
444 switch(node.nodeType) {
445 case Node.ELEMENT_NODE:
446 return _getClassName(node) == MN_PH_CLASS;
447 case Node.TEXT_NODE:
448 return !immediate && node.parentNode && _getClassName(node.parentNode) == MN_PH_CLASS;
449 }
450 return false;
451 },
452
453 /**
454 * @param {Node} editSection A editible section to create the editable section placeholder for
455 * @return {Node} An editable section placeholder element.
456 */
457 createESPlaceholder : function(editSection) {
458 var phHTML = this.getEditProperties(editSection).phMarkup || "&nbsp;";
459 var ph = $createElement("span");
460 _setClassName(ph, ES_PH_CLASS);
461 ph.innerHTML = phHTML;
462 return ph;
463 },
464
465 /**
466 * Determines whether a dom node is (part of) an editable section placeholder.
467 *
468 * @param {Node} node A dom node to test
469 *
470 * @param {Boolean} immediate True to only test if node is a placeholder.
471 * False to also test if the nodes descendants is a placeholder.
472 *
473 * @return {Boolean} True if node is (part of) a editable section placeholder. False if it is not
474 */
475 isESPlaceHolder: function(node, immediate){
476
477 while (node) {
478 if (node.nodeType == Node.ELEMENT_NODE) {
479 var clsName = _getClassName(node);
480 if (clsName == ES_PH_CLASS)
481 return true;
482 }
483 if (immediate) break;
484 node = node.parentNode;
485 }
486 return false;
487 },
488
489 /**
490 * Create and adds a new edit section to the document. Sets the classname for the given editable section.
491 * The is required when adding new editable sections <em>after initialization</em> so dedit can track
492 * its changes.
493 * <br/>
494 * Adds placeholders if they are needed.
495 *
496 * @param {Node} esEle The editable section to register
497 * @param {String} propertySetName The name of the property set to use. Can be null/empty for default set.
498 *
499 * @see de.doc.removeEditSection For removing a editable section.
500 */
501 registerEditSection : function(esEle, propertySetName) {
502
503 // Set class name as editable
504 var clsName = _getClassName(esEle);
505 _setClassName(esEle, (clsName ? clsName + " " : "") + _ES_CLASS_PREFIX + (propertySetName ? "-" + propertySetName : ""));
506
507 // Add an editable section placeholder if it needs it
508 if (!_doesContainTanglableDescendant(esEle))
509 esEle.appendChild(this.createESPlaceholder(esEle));
510
511 this.fireEvent("SectionAdded", esEle); // NB: Changes module listens
512 },
513
514 /**
515 * Unregisters an editable section from the document.
516 * Sets the classname for the given editable section
517 *
518 * @param {Node} esEle The edit section to remove from the document.
519 */
520 unregisterEditSection: function(esEle) {
521
522 // Strip classname of editable section prefix
523 var clsName = _getClassName(esEle);
524 if (clsName)
525 _setClassName(esEle, clsName.replace(new RegExp("^|\s" + _ES_CLASS_PREFIX + "\S*$", "g"), ""))
526
527 this.fireEvent("SectionRemoved", esEle); // NB: Changes module listens
528 }
529
530 }; // End doc namespace
531
532})();
Note: See TracBrowser for help on using the repository browser.