1 /** 2 * @fileoverview A user interface component to visually edit 3 * XML Mixed Content like in a Rich Text Editor. 4 */ 5 6 goog.provide('xrx.richxml'); 7 8 9 goog.require('goog.dom'); 10 goog.require('xrx.codemirror'); 11 goog.require('xrx.label'); 12 goog.require('xrx.richxml.cursor'); 13 goog.require('xrx.richxml.mode'); 14 goog.require('xrx.richxml.tagname'); 15 goog.require('xrx.stream'); 16 goog.require('xrx.view'); 17 18 19 20 /** 21 * @constructor 22 */ 23 xrx.richxml = function(element, opt_tagname) { 24 25 26 27 goog.base(this, element); 28 29 30 31 /** 32 * @private 33 */ 34 this.cursor_; 35 36 37 38 /** 39 * @private 40 */ 41 this.tagname_ = opt_tagname; 42 }; 43 goog.inherits(xrx.richxml, xrx.codemirror); 44 45 46 47 xrx.richxml.prototype.cursor = function() { 48 return this.cursor_ || new xrx.richxml.cursor(this); 49 }; 50 51 52 53 xrx.richxml.prototype.tagname = function() { 54 return this.tagname_ ? this.tagname_ : this.tagname_ = new xrx.richxml.tagname(); 55 }; 56 57 58 xrx.richxml.placeholder = { 59 startTag: unescape('%BB'), 60 endTag: unescape('%AB'), 61 emptyTag: unescape('%D7') 62 }; 63 64 65 66 xrx.richxml.placeholder.matches = function(ch) { 67 68 for(var p in xrx.richxml.placeholder) { 69 if (xrx.richxml.placeholder[p] === ch) { 70 return true; 71 } 72 } 73 return false; 74 }; 75 76 77 78 xrx.richxml.prototype.cursorIsNearTag = function() { 79 return xrx.richxml.placeholder.matches(this.cursor().leftTokenOutside().string); 80 }; 81 82 83 84 xrx.richxml.className = { 85 startTag: 'richxml-start-tag', 86 startTagActive: 'richxml-start-tag-active', 87 endTag: 'richxml-end-tag', 88 endTagActive: 'richxml-end-tag-active', 89 emptyTag: 'richxml-empty-tag', 90 emptyTagActive: 'richxml-empty-tag-active', 91 notTag: 'richxml-not-tag', 92 notTagActive: 'richxml-not-tag-active' 93 }; 94 95 96 97 /** 98 * @private 99 */ 100 xrx.richxml.prototype.transformXml_ = function(xml) { 101 var visualXml = ''; 102 var self = this; 103 var stream = new xrx.stream(xml); 104 105 stream.rowStartTag = function(offset, length1, length2) { 106 visualXml += xrx.richxml.placeholder.startTag; 107 if(length1 !== length2) visualXml += xml.substr(offset + length1, length2 - length1); 108 }; 109 110 stream.rowEndTag = function(offset, length1, length2) { 111 visualXml += xrx.richxml.placeholder.endTag; 112 if(length1 !== length2) visualXml += xml.substr(offset + length1, length2 - length1); 113 }; 114 115 stream.rowEmptyTag = function(offset, length1, length2) { 116 visualXml += xrx.richxml.placeholder.emptyTag; 117 if(length1 !== length2) visualXml += xml.substr(offset + length1, length2 - length1); 118 }; 119 120 stream.forward(); 121 122 return visualXml; 123 }; 124 125 126 127 xrx.richxml.prototype.options = { 128 'mode': 'richxml' 129 }; 130 131 132 133 xrx.richxml.prototype.refresh = function() { 134 var visualXml = this.transformXml_(this.getNode().xml()); 135 this.codemirror_.setValue(visualXml); 136 }; 137 138 139 140 xrx.richxml.prototype.clear = function() { 141 this.unmarkActiveTags(); 142 this.hideElements(); 143 this.hideAttributes(); 144 this.hideTagName(); 145 }; 146 147 148 149 xrx.richxml.prototype.unmarkActiveTags = function() { 150 this.markActiveTags(true); 151 }; 152 153 154 155 xrx.richxml.prototype.markActiveTags = function(unmarkflag) { 156 157 var cm = this.codemirror_; 158 var cursor = this.cursor(); 159 var self = this; 160 161 var markedElements = function() { 162 return self.markedElements_ || (self.markedElements_ = []); 163 }; 164 165 var correspondingTag = function() { 166 var text = cm.getValue(); 167 var index = cm.indexFromPos(cm.getCursor()) - 1; 168 var ch; 169 var stack = 0; 170 171 if(cursor.leftTokenOutside().string === xrx.richxml.placeholder.startTag) { 172 173 for(var i = index; i < text.length; i++) { 174 ch = text[i]; 175 if(ch === xrx.richxml.placeholder.endTag) stack -= 1; 176 if(ch === xrx.richxml.placeholder.startTag) stack += 1; 177 if(stack === 0) { 178 index = i + 1; 179 break; 180 } 181 } 182 } else if(cursor.leftTokenOutside().string === xrx.richxml.placeholder.endTag) { 183 184 for(var i = index; i >= 0; i--) { 185 ch = text[i]; 186 if(ch == xrx.richxml.placeholder.endTag) stack += 1; 187 if(ch == xrx.richxml.placeholder.startTag) stack -= 1; 188 if(stack == 0) { 189 index = i + 1; 190 break; 191 } 192 } 193 } else {}; 194 195 return cm.posFromIndex(index); 196 }; 197 198 var mark = function(from, to, style) { 199 var mark = cm.markText(from, to, style); 200 201 markedElements().push(mark); 202 }; 203 204 var unmark = function() { 205 206 for(var i = 0; i < markedElements().length; ++i) { 207 markedElements()[i].clear(); 208 } 209 self.markedElements_ = []; 210 }; 211 212 unmark(); 213 if (!this.cursorIsNearTag()) return; 214 if (unmarkflag) return; 215 216 var firstMark = cursor.leftTokenOutside().string === xrx.richxml.placeholder.startTag ? 217 xrx.richxml.className.startTagActive : xrx.richxml.className.endTagActive; 218 var secondMark = cursor.leftTokenOutside().string === xrx.richxml.placeholder.startTag ? 219 xrx.richxml.className.endTagActive : xrx.richxml.className.startTagActive; 220 221 // mark matched element 222 mark( 223 { line: cursor.leftPosition().line, ch: cursor.leftPosition().ch - 1 }, 224 { line: cursor.leftPosition().line, ch: cursor.leftPosition().ch }, 225 { className: firstMark } 226 ); 227 228 if (cursor.leftTokenOutside().string === xrx.richxml.placeholder.emptyTag) return; 229 230 // mark corresponding element 231 var corresponding = correspondingTag(); 232 mark( 233 { line: corresponding.line, ch: corresponding.ch - 1 }, 234 { line: corresponding.line, ch: corresponding.ch }, 235 { className: secondMark } 236 ); 237 238 // mark text 239 var from = { line: cursor.leftPosition().line, ch: cursor.leftPosition().ch }; 240 var to = { line: corresponding.line, ch: corresponding.ch - 1 }; 241 cursor.leftTokenOutside().string === xrx.richxml.placeholder.startTag ? 242 mark(from, to, { className: xrx.richxml.className.notTagActive }) : 243 mark(to, from, { className: xrx.richxml.className.notTagActive }); 244 }; 245 246 247 248 xrx.richxml.prototype.showElements = function() { 249 250 }; 251 252 253 254 xrx.richxml.prototype.hideElements = function() { 255 256 }; 257 258 259 260 xrx.richxml.prototype.showAttributes = function() { 261 262 }; 263 264 265 266 xrx.richxml.prototype.hideAttributes = function() { 267 268 }; 269 270 271 272 xrx.richxml.prototype.showTagName = function() { 273 this.cursorIsNearTag() ? this.tagname().show(this.cursor().getNode()) : 274 this.hideTagName(); 275 }; 276 277 278 279 xrx.richxml.prototype.hideTagName = function() { 280 this.tagname().hide(); 281 }; 282 283 284 285 xrx.richxml.prototype.setReadonly = function(readonly) { 286 readonly ? this.codemirror_.setOption('readOnly', true) : 287 this.codemirror_.setOption('readOnly', false); 288 }; 289 290 291 292 xrx.richxml.prototype.eventBlur = function() { 293 this.clear(); 294 }; 295 296 297 298 xrx.richxml.prototype.cursorActivitySelection = function() { 299 var cm = this.codemirror_; 300 var cursor = this.cursor(); 301 var valid = false; 302 303 if(cursor.leftAtStartPosition() || cursor.rightAtEndPosition()) return; 304 305 var leftTokenOutside = cursor.leftTokenOutside().state.context.token; 306 var rightTokenInside = cursor.rightTokenInside().state.context.token; 307 var leftToken 308 var rightToken 309 }; 310 311 312 313 /** 314 * 315 */ 316 xrx.richxml.prototype.eventCursorActivity = function() { 317 var cm = this.codemirror_; 318 319 if (cm.somethingSelected()) { 320 this.setReadonly(true); 321 this.cursorActivitySelection(); 322 } else { 323 this.setReadonly(false); 324 this.markActiveTags(); 325 this.showElements(); 326 this.showAttributes(); 327 this.showTagName(); 328 } 329 }; 330 331 332 333 334 335