styled-textbox.xml (21911B)
1 <?xml version="1.0"?> 2 <!-- 3 ***** BEGIN LICENSE BLOCK ***** 4 5 Copyright © 2009 Center for History and New Media 6 George Mason University, Fairfax, Virginia, USA 7 http://zotero.org 8 9 This file is part of Zotero. 10 11 Zotero is free software: you can redistribute it and/or modify 12 it under the terms of the GNU Affero General Public License as published by 13 the Free Software Foundation, either version 3 of the License, or 14 (at your option) any later version. 15 16 Zotero is distributed in the hope that it will be useful, 17 but WITHOUT ANY WARRANTY; without even the implied warranty of 18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 GNU Affero General Public License for more details. 20 21 You should have received a copy of the GNU Affero General Public License 22 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 23 24 ***** END LICENSE BLOCK ***** 25 --> 26 27 <!DOCTYPE bindings SYSTEM "chrome://zotero/locale/zotero.dtd"> 28 <bindings xmlns="http://www.mozilla.org/xbl" 29 xmlns:html="http://www.w3.org/1999/xhtml" 30 xmlns:xbl="http://www.mozilla.org/xbl" 31 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> 32 <binding id="styled-textbox"> 33 <implementation> 34 <field name="_editable"/> 35 <field name="_mode"/> 36 <field name="_format"/> 37 <field name="_changed"/> 38 <field name="_loadHandler"/> 39 <field name="_commandString"/> 40 <field name="_eventHandler"/> 41 <field name="_editor"/> 42 <field name="_value"/> 43 <field name="_timer"/> 44 <field name="_focus"/> 45 <field name="_constructed"/> 46 <field name="_loadOnConstruct"/> 47 48 <constructor><![CDATA[ 49 this.mode = this.getAttribute('mode'); 50 51 this._iframe = document.getAnonymousElementByAttribute(this, "anonid", "rt-view"); 52 53 // Atomic units, HTML -> RTF (cleanup) 54 //[/<\/p>(?!\s*$)/g, "\\par{}"], 55 //[/ /g, " "], 56 //[/\u00A0/g, " "], 57 this._htmlRTFmap = [ 58 [/<br \/>/g, "\x0B"], 59 [/<span class=\"tab\"> <\/span>/g, "\\tab{}"], 60 [/‘/g, "‘"], 61 [/’/g, "’"], 62 [/“/g, "“"], 63 [/”/g, "”"], 64 [/ /g, "\u00A0"], 65 [/"(\w)/g, "“$1"], 66 [/([\w,.?!])"/g, "$1”"], 67 [/<p>/g, ""], 68 [/<\/?div[^>]*>/g, ""] 69 ]; 70 71 // Atomic units, RTF -> HTML (cleanup) 72 this._rtfHTMLmap = [ 73 [/\\uc0\{?\\u([0-9]+)\}?(?:{}| )?/g, function(wholeStr, aCode) { return String.fromCharCode(aCode) }], 74 [/\\tab(?:\{\}| )/g, '<span class="tab"> </span>'], 75 [/(?:\\par{}|\\\r?\n)/g, "</p><p>"] 76 ]; 77 78 this.prepare = function() { 79 // DEBUG: Does this actually happen? 80 if (this.prepared) return; 81 82 // Tag data 83 var _rexData = [ 84 [ 85 [ 86 ["<span +style=\"font-variant: *small-caps;\">"], 87 ["{\\scaps ", "{\\scaps{}"] 88 ], 89 [ 90 ["<\/span>"], 91 ["}"] 92 ] 93 ], 94 [ 95 [ 96 ["<span +style=\"text-decoration: *underline;\">"], 97 ["{\\ul{}", "{\\ul "] 98 ], 99 [ 100 ["<\/span>"], 101 ["}"] 102 ] 103 ], 104 [ 105 [ 106 ["<sup>"], 107 ["\\super ", "\\super{}"] 108 ], 109 [ 110 ["</sup>"], 111 ["\\nosupersub{}", "\\nosupersub "] 112 ] 113 ], 114 [ 115 [ 116 ["<sub>"], 117 ["\\sub ", "\\sub{}"] 118 ], 119 [ 120 ["</sub>"], 121 ["\\nosupersub{}", "\\nosupersub "] 122 ] 123 ], 124 [ 125 [ 126 ["<em>"], 127 ["{\\i{}", "{\\i "] 128 ], 129 [ 130 ["</em>"], 131 ["}"] 132 ] 133 ], 134 [ 135 [ 136 ["<i>"], 137 ["{\\i{}", "{\\i "] 138 ], 139 [ 140 ["</i>"], 141 ["}"] 142 ] 143 ], 144 [ 145 [ 146 ["<b>"], 147 ["{\\b{}", "{\\b "] 148 ], 149 [ 150 ["</b>"], 151 ["}"] 152 ] 153 ], 154 [ 155 [ 156 ["<strong>"], 157 ["{\\b{}", "{\\b "] 158 ], 159 [ 160 ["</strong>"], 161 ["}"] 162 ] 163 ], 164 [ 165 [ 166 ["<span +style=\"font-variant: *normal;\">"], 167 ["{\\scaps0{}", "{\\scaps0 "] 168 ], 169 [ 170 ["</span>"], 171 ["}"] 172 ] 173 ], 174 [ 175 [ 176 ["<span +style=\"font-style: *normal;\">"], 177 ["{\\i0{}", "{\\i0 "] 178 ], 179 [ 180 ["</span>"], 181 ["}"] 182 ] 183 ], 184 [ 185 [ 186 ["<span +style=\"font-weight: *normal;\">"], 187 ["{\\b0{}", "{\\b0 "] 188 ], 189 [ 190 ["</span>"], 191 ["}"] 192 ] 193 ] 194 ]; 195 196 function longestFirst(a, b) { 197 if (a.length < b.length) { 198 return 1; 199 } else if (a.length > b.length) { 200 return -1; 201 } else { 202 return 0; 203 } 204 } 205 206 function normalizeRegExpString(str) { 207 if (!str) return str; 208 return str.replace(/\s+/g, " ") 209 .replace(/(?:[\+]|\s[\*])/g, "") 210 .replace(/[\']/g, '\"') 211 .replace(/:\s/g, ":"); 212 } 213 214 this.normalizeRegExpString = normalizeRegExpString; 215 216 function composeRex(rexes, noGlobal) { 217 var lst = []; 218 for (var rex in rexes) { 219 lst.push(rex); 220 } 221 lst.sort(longestFirst); 222 var rexStr = "(?:" + lst.join("|") + ")"; 223 return new RegExp(rexStr, "g"); 224 } 225 226 // Create splitting regexps 227 function splitRexMaker(segment) { 228 var rexes = {}; 229 for (var i=0,ilen=_rexData.length; i < ilen; i++) { 230 for (var j=0,jlen=_rexData[i].length; j < jlen; j++) { 231 for (var k=0,klen=_rexData[i][j][segment].length; k < klen; k++) { 232 rexes[_rexData[i][j][segment][k].replace("\\", "\\\\")] = true; 233 } 234 } 235 } 236 var ret = composeRex(rexes, true); 237 return ret; 238 } 239 this.rtfHTMLsplitRex = splitRexMaker(1); 240 this.htmlRTFsplitRex = splitRexMaker(0); 241 242 // Create open-tag sniffing regexp 243 function openSniffRexMaker(segment) { 244 var rexes = {}; 245 for (var i=0,ilen=_rexData.length; i < ilen; i++) { 246 for (var j=0,jlen=_rexData[i][0][segment].length; j < jlen; j++) { 247 rexes[_rexData[i][0][segment][j].replace("\\", "\\\\")] = true; 248 } 249 } 250 return composeRex(rexes); 251 } 252 this.rtfHTMLopenSniffRex = openSniffRexMaker(1); 253 this.htmlRTFopenSniffRex = openSniffRexMaker(0); 254 255 // Create open-tag remapper 256 function openTagRemapMaker(segment) { 257 var ret = {}; 258 for (var i=0,ilen=_rexData.length; i < ilen; i++) { 259 var primaryVal = normalizeRegExpString(_rexData[i][0][segment][0]); 260 for (var j=0,jlen=_rexData[i][0][segment].length; j < jlen; j++) { 261 var key = normalizeRegExpString(_rexData[i][0][segment][j]); 262 ret[key] = primaryVal; 263 } 264 } 265 return ret; 266 } 267 268 this.rtfHTMLopenTagRemap = openTagRemapMaker(1); 269 this.htmlRTFopenTagRemap = openTagRemapMaker(0); 270 271 // Create open-tag-keyed close-tag sniffing regexps 272 function closeTagRexMaker(segment) { 273 var ret = {}; 274 var rexes = {}; 275 for (var i=0,ilen=_rexData.length; i < ilen; i++) { 276 var primaryVal = _rexData[i][0][segment][0]; 277 for (var j=0,jlen=_rexData[i][1][segment].length; j < jlen; j++) { 278 rexes[_rexData[i][1][segment][j]] = true; 279 } 280 ret[primaryVal] = composeRex(rexes); 281 } 282 return ret; 283 } 284 this.rtfHTMLcloseTagRex = closeTagRexMaker(1); 285 this.htmlRTFcloseTagRex = closeTagRexMaker(0); 286 287 // Create open-tag-keyed open/close tag registry 288 function tagRegistryMaker(segment) { 289 var antisegment = 1; 290 if (segment == 1) { 291 antisegment = 0; 292 } 293 var ret = {}; 294 for (var i=0,ilen=_rexData.length; i < ilen; i++) { 295 var primaryVal = normalizeRegExpString(_rexData[i][0][segment][0]); 296 ret[primaryVal] = { 297 open: normalizeRegExpString(_rexData[i][0][antisegment][0]), 298 close: _rexData[i][1][antisegment][0] 299 } 300 } 301 return ret; 302 } 303 304 this.rtfHTMLtagRegistry = tagRegistryMaker(1); 305 this.htmlRTFtagRegistry = tagRegistryMaker(0); 306 307 this.prepared = true; 308 } 309 this.prepare(); 310 311 this.getSplit = function(mode, txt) { 312 if (!txt) return []; 313 var splt = txt.split(this[mode + "splitRex"]); 314 var mtch = txt.match(this[mode + "splitRex"]); 315 var lst = [splt[0]]; 316 for (var i=1,ilen=splt.length; i < ilen; i++) { 317 lst.push(mtch[i-1]); 318 lst.push(splt[i]); 319 } 320 return lst; 321 } 322 323 this.getOpenTag = function(mode, str) { 324 var m = str.match(this[mode + "openSniffRex"]); 325 if (m) { 326 m = this[mode + "openTagRemap"][this.normalizeRegExpString(m[0])]; 327 } 328 return m; 329 } 330 331 this.convert = function(mode, txt) { 332 var lst = this.getSplit(mode, txt); 333 var sdepth = 0; 334 var depth = 0; 335 for (var i=1,ilen=lst.length; i < ilen; i += 2) { 336 var openTag = this.getOpenTag(mode, lst[i]); 337 if (openTag) { 338 sdepth++; 339 depth = sdepth; 340 for (var j=(i+2),jlen=lst.length; j < jlen; j += 2) { 341 var closeTag = !this.getOpenTag(mode, lst[j]); 342 if (closeTag) { 343 if (depth === sdepth && lst[j].match(this[mode + "closeTagRex"][openTag])) { 344 lst[i] = this[mode + "tagRegistry"][openTag].open; 345 lst[j] = this[mode + "tagRegistry"][openTag].close; 346 break; 347 } 348 depth--; 349 } else { 350 depth++; 351 } 352 } 353 } else { 354 sdepth--; 355 } 356 } 357 return lst.join(""); 358 } 359 360 this.htmlToRTF = function(txt) { 361 txt = this.convert("htmlRTF", txt); 362 for (var i=0,ilen=this._htmlRTFmap.length; i < ilen; i++) { 363 var entry = this._htmlRTFmap[i]; 364 txt = txt.replace(entry[0], entry[1]); 365 } 366 txt = Zotero.Utilities.unescapeHTML(txt); 367 txt = txt.replace(/[\x7F-\uFFFF]/g, function(aChar) { return "\\uc0\\u"+aChar.charCodeAt(0).toString()+"{}"}); 368 return txt.trim(); 369 } 370 371 this.rtfToHTML = function(txt) { 372 for (var i=0,ilen=this._rtfHTMLmap.length; i < ilen; i++) { 373 var entry = this._rtfHTMLmap[i]; 374 txt = txt.replace(entry[0], entry[1]); 375 } 376 txt = this.convert("rtfHTML", txt); 377 return txt.trim(); 378 } 379 380 this._constructed = true; 381 382 // Don't load if a value hasn't yet been set 383 if (this._loadOnConstruct) { 384 this._load(); 385 } 386 ]]></constructor> 387 388 <property name="mode"> 389 <getter><![CDATA[ 390 if (!this._mode) { 391 throw ("mode is not defined in styled-textbox.xml"); 392 } 393 return this._mode; 394 ]]></getter> 395 <setter><![CDATA[ 396 Zotero.debug("Setting mode to " + val); 397 switch (val) { 398 case 'note': 399 this._eventHandler = function (event) { 400 // Necessary in Fx32+ 401 if (event.wrappedJSObject) { 402 event = event.wrappedJSObject; 403 } 404 405 var commandEvent = false; 406 407 if (Zotero.Prefs.get('debugNoteEvents')) { 408 Zotero.debug(event.type); 409 Zotero.debug(event.which); 410 } 411 switch (event.type) { 412 case 'keydown': 413 // Handle forward-delete, which doesn't register as a keypress 414 // when a selection is cleared 415 if (event.which == event.DOM_VK_DELETE) { 416 this._changed = true; 417 commandEvent = true; 418 } 419 break; 420 421 case 'keypress': 422 // Ignore keypresses that don't change 423 // any text 424 //Zotero.debug(event.which); 425 if (!event.which && 426 event.keyCode != event.DOM_VK_DELETE && 427 event.keyCode != event.DOM_VK_BACK_SPACE) { 428 //Zotero.debug("Not a char"); 429 return; 430 } 431 this._changed = true; 432 commandEvent = true; 433 break; 434 435 // 'change' includes text added via drag-and-drop 436 case 'change': 437 case 'undo': 438 case 'redo': 439 this._changed = true; 440 commandEvent = true; 441 break; 442 443 case 'ZoteroLinkClick': 444 var zp = typeof ZoteroPane != 'undefined' 445 ? ZoteroPane 446 : window.opener.ZoteroPane; 447 zp.loadURI(event.value); 448 break; 449 450 default: 451 return; 452 } 453 454 // Trigger command on change 455 if (commandEvent && this.timeout) { 456 if (this._timer) { 457 clearTimeout(this._timer); 458 } 459 460 this._timer = setTimeout(function () { 461 var attr = this.getAttribute('oncommand'); 462 attr = attr.replace('this', 'thisObj'); 463 var func = new Function('thisObj', 'event', attr); 464 func(this, event); 465 }.bind(this), this.timeout); 466 } 467 468 return true; 469 }.bind(this); 470 break; 471 472 case 'integration': 473 break; 474 475 default: 476 throw ("Invalid mode '" + val + "' in styled-textbox.xml"); 477 } 478 return this._mode = val; 479 ]]></setter> 480 </property> 481 482 <!-- Sets or returns formatting (currently, HTML or Integration) of rich text box --> 483 <property name="initialized"> 484 <getter><![CDATA[ 485 return !!this._editor; 486 ]]></getter> 487 </property> 488 489 <!-- Sets or returns formatting (currently, HTML or Integration) of rich text box --> 490 <property name="format"> 491 <getter><![CDATA[ 492 return this._format; 493 ]]></getter> 494 <setter><![CDATA[ 495 return this._format = val; 496 ]]></setter> 497 </property> 498 499 <!-- Sets or returns contents of rich text box --> 500 <property name="value"> 501 <getter><![CDATA[ 502 if (!this._editor) { 503 return null; 504 } 505 506 var output = this._editor.getContent().trim(); 507 508 if(this._format == "RTF") { 509 // strip divs 510 if(output.substr(0, 5) == "<div>" && output.substr(-6) == "</div>") { 511 output = output.slice(0, output.length-6).slice(5).trim(); 512 } 513 output = this.htmlToRTF(output) 514 } 515 516 return output; 517 ]]></getter> 518 <setter><![CDATA[ 519 if (self._timer) { 520 clearTimeout(self._timer); 521 } 522 523 if(!this._editor) { 524 Zotero.debug('No editor yet'); 525 526 this._value = val; 527 if (!this._constructed) { 528 Zotero.debug('Styled textbox not yet constructed', 2); 529 this._loadOnConstruct = true; 530 } 531 else if (!this._loaded) { 532 this._load(); 533 } 534 return ; 535 } 536 537 if (this.value == val) { 538 Zotero.debug("Textbox value hasn't changed"); 539 this._changed = false; 540 return; 541 } 542 543 var html = val; 544 if(this._format == "RTF") { 545 var bodyStyle = ""; 546 if(html.substr(0, 3) == "\\li") { 547 // try to show paragraph formatting 548 var returnIndex = html.indexOf("\r\n"); 549 550 var tags = html.substr(1, returnIndex).split("\\"); 551 html = html.substr(returnIndex+2); 552 553 for(var i=0; i<tags.length; i++) { 554 var tagName = tags[i].substr(0, 2); 555 var tagValue = tags[i].substring(2, tags[i].length-1); 556 if(tagName == "li") { 557 var li = parseInt(tagValue, 10); 558 } else if(tagName == "fi") { 559 var fi = parseInt(tagValue, 10); 560 } 561 } 562 563 // don't negatively indent 564 if(fi < 0 && li == 0) li = -fi; 565 566 bodyStyle = "margin-left:"+(li/20+6)+"pt;text-indent:"+(fi/20)+"pt;"; 567 } 568 569 html = this.rtfToHTML(html); 570 571 html = '<div style="'+bodyStyle+'"><p>'+html+"</p></div>"; 572 } 573 574 Zotero.debug("Setting content to " + html); 575 576 this._editor.setContent(html); 577 this._changed = false; 578 return val; 579 ]]></setter> 580 </property> 581 582 <property name="timeout" 583 onset="this.setAttribute('timeout', val); return val;" 584 onget="return parseInt(this.getAttribute('timeout')) || 0;"/> 585 586 <property name="changed" onget="return this._changed;" onset="this._changed = !!val;"/> 587 588 <method name="focus"> 589 <body> 590 <![CDATA[ 591 if (this._editor) { 592 this._iframe.focus(); 593 this._editor.focus(); 594 this._focus = false; 595 } 596 else { 597 this._focus = true; 598 } 599 ]]> 600 </body> 601 </method> 602 603 <method name="clearUndo"> 604 <body> 605 <![CDATA[ 606 if (this._editor) { 607 this._editor.undoManager.clear(); 608 this._editor.undoManager.add(); 609 } 610 ]]> 611 </body> 612 </method> 613 614 <method name="onInit"> 615 <parameter name="callback"/> 616 <body><![CDATA[ 617 if (this.initialized) { 618 callback(this._editor); 619 } 620 else { 621 if (!this._onInitCallbacks) { 622 this._onInitCallbacks = []; 623 } 624 this._onInitCallbacks.push(callback); 625 } 626 ]]></body> 627 </method> 628 629 <field name="_loaded"/> 630 <method name="_load"> 631 <body> 632 <![CDATA[ 633 this._loaded = true; 634 635 // Unless we find a better way, use a separate HTML file 636 // for read-only mode 637 var htmlFile = this.mode + (this.getAttribute('readonly') != 'true' ? "" : "view"); 638 639 var ios = Components.classes["@mozilla.org/network/io-service;1"]. 640 getService(Components.interfaces.nsIIOService); 641 var uri = ios.newURI("resource://zotero/tinymce/" + htmlFile + ".html", null, null); 642 643 // Pass directionality (LTR/RTL) and locale in URL 644 uri.spec += "?locale=" + encodeURIComponent(Zotero.locale) 645 + "&dir=" + Zotero.dir; 646 647 648 Zotero.debug("Loading " + uri.spec); 649 650 // Register handler for deferred setting of content 651 var self = this; 652 var matchTo = null; 653 var listener = function(e) { 654 var win = self._iframe.contentWindow; 655 var SJOW = win.wrappedJSObject; 656 657 // only fire if the target matches, or _zoteroMatchTo, which we set last 658 // time the target matched, matches 659 if(e.target !== self._iframe.contentDocument 660 && (!SJOW._zoteroMatchTo || SJOW._zoteroMatchTo !== matchTo)) return; 661 662 if (!SJOW.tinyMCE) { 663 Zotero.getInstalledExtensions().then(function(exts) { 664 for (let ext of exts) { 665 if (ext.indexOf('NoScript') != -1 && ext.indexOf('disabled') == -1) { 666 var doc = win.document; 667 var div = doc.getElementById('tinymce'); 668 var warning = doc.createElement('div'); 669 warning.id = 'noScriptWarning'; 670 var str = "The NoScript extension is preventing Zotero " 671 + "from displaying notes. To use NoScript and Zotero together, " 672 + "whitelist the 'file:' scheme in the NoScript preferences " 673 + "and restart " + Zotero.appName + "."; 674 warning.appendChild(document.createTextNode(str)); 675 div.appendChild(warning); 676 break; 677 } 678 } 679 }); 680 return; 681 } 682 683 if (!SJOW.zoteroInit) { 684 SJOW.zoteroInit = function(editor) { 685 // Necessary in Fx32+ 686 if (editor.wrappedJSObject) { 687 self._editor = editor.wrappedJSObject; 688 } 689 else { 690 self._editor = editor; 691 } 692 if (self._value) { 693 self.value = self._value; 694 695 // Prevent undoing to empty note after initialization 696 self._editor.undoManager.clear(); 697 self._editor.undoManager.add(); 698 } 699 if (self._focus) { 700 setTimeout(function () { 701 self._iframe.focus(); 702 self._editor.focus(); 703 }); 704 self._focus = false; 705 } 706 707 // Add CSS rules to notes 708 if (self.mode == 'note') { 709 let fontSize = Zotero.Prefs.get('note.fontSize'); 710 // Fix empty old font prefs before a value was enforced 711 if (fontSize < 6) { 712 fontSize = 11; 713 } 714 var css = "body#zotero-tinymce-note, " 715 + "body#zotero-tinymce-note p, " 716 + "body#zotero-tinymce-note th, " 717 + "body#zotero-tinymce-note td, " 718 + "body#zotero-tinymce-note pre { " 719 + "font-size: " + fontSize + "px; " 720 + "} " 721 + "body#zotero-tinymce-note, " 722 + "body#zotero-tinymce-note p { " 723 + "font-family: " 724 + Zotero.Prefs.get('note.fontFamily') + "; " 725 + "}" 726 + Zotero.Prefs.get('note.css'); 727 728 var doc = editor.contentDocument; 729 var head = doc.getElementsByTagName("head")[0]; 730 var style = doc.createElement("style"); 731 style.innerHTML = css; 732 head.appendChild(style); 733 } 734 735 let cb; 736 if (this._onInitCallbacks) { 737 while (cb = this._onInitCallbacks.shift()) { 738 cb(this._editor); 739 } 740 } 741 }.bind(this); 742 } 743 744 var editor = SJOW.tinyMCE.get("tinymce"); 745 if (!editor) { 746 Zotero.debug("editor not ready"); 747 748 // this is a hack; I'm not sure why we can't identify the event target 749 // next time without it, but apparently we can't 750 matchTo = Zotero.randomString(); 751 SJOW._zoteroMatchTo = matchTo; 752 753 // Not ready yet 754 return; 755 } 756 757 self._iframe.removeEventListener("DOMContentLoaded", listener, false); 758 759 if (self._eventHandler) { 760 win.wrappedJSObject.zoteroHandleEvent = self._eventHandler; 761 } 762 763 // Run Cut/Copy/Paste with chrome privileges 764 win.wrappedJSObject.zoteroExecCommand = function (doc, command, ui, value) { 765 return doc.execCommand(command, ui, value); 766 } 767 }.bind(this); 768 769 this._iframe.addEventListener("DOMContentLoaded", listener, false); 770 771 this._iframe.webNavigation.loadURI(uri.spec, 772 Components.interfaces.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY, null, null, null); 773 ]]> 774 </body> 775 </method> 776 777 </implementation> 778 779 <content> 780 <xul:iframe flex="1" anonid="rt-view" class="rt-view" type="content" 781 xbl:inherits="onfocus,onblur,flex,width,height,hidden" 782 style="overflow: hidden"/> 783 </content> 784 </binding> 785 </bindings>