noteeditor.xml (18106B)
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 <bindings xmlns="http://www.mozilla.org/xbl" 28 xmlns:xbl="http://www.mozilla.org/xbl" 29 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> 30 31 <binding id="note-editor"> 32 <resources> 33 <stylesheet src="chrome://zotero/skin/bindings/noteeditor.css"/> 34 <stylesheet src="chrome://zotero-platform/content/noteeditor.css"/> 35 </resources> 36 37 <implementation> 38 <!-- 39 Public properties 40 --> 41 <field name="editable">false</field> 42 <field name="saveOnEdit">false</field> 43 <field name="displayTags">false</field> 44 <field name="displayRelated">false</field> 45 <field name="displayButton">false</field> 46 47 <field name="buttonCaption"/> 48 <field name="parentClickHandler"/> 49 <field name="keyDownHandler"/> 50 <field name="commandHandler"/> 51 <field name="clickHandler"/> 52 53 <!-- Modes are predefined settings groups for particular tasks --> 54 <field name="_mode">"view"</field> 55 <property name="mode" onget="return this._mode;"> 56 <setter> 57 <![CDATA[ 58 // Duplicate default property settings here 59 this.editable = false; 60 this.saveOnEdit = false; 61 this.displayTags = false; 62 this.displayRelated = false; 63 this.displayButton = false; 64 65 switch (val) { 66 case 'view': 67 case 'merge': 68 // If there's an existing editor, mark it as read-only. This allows for 69 // disabling an existing editable note (e.g., if there's a save error). 70 if (this.noteField) { 71 this.noteField.onInit(ed => ed.setMode('readonly')); 72 } 73 break; 74 75 case 'edit': 76 this.editable = true; 77 this.saveOnEdit = true; 78 this.parentClickHandler = this.selectParent; 79 this.keyDownHandler = this.handleKeyDown; 80 this.commandHandler = this.save; 81 this.displayTags = true; 82 this.displayRelated = true; 83 break; 84 85 default: 86 throw ("Invalid mode '" + val + "' in noteeditor.xml"); 87 } 88 89 this._mode = val; 90 document.getAnonymousNodes(this)[0].setAttribute('mode', val); 91 this._id('links-box').mode = val; 92 ]]> 93 </setter> 94 </property> 95 96 <field name="_parentItem"/> 97 <property name="parentItem" onget="return this._parentItem;"> 98 <setter> 99 <![CDATA[ 100 this._parentItem = this._id('links-box').parentItem = val; 101 ]]> 102 </setter> 103 </property> 104 105 <field name="_mtime"/> 106 107 <field name="_item"/> 108 <property name="item" onget="return this._item;"> 109 <setter><![CDATA[ 110 this._item = val; 111 // TODO: use clientDateModified instead 112 this._mtime = val.getField('dateModified'); 113 114 var parentKey = this.item.parentKey; 115 if (parentKey) { 116 this.parentItem = Zotero.Items.getByLibraryAndKey(this.item.libraryID, parentKey); 117 } 118 119 this._id('links-box').item = this.item; 120 121 this.refresh(); 122 ]]></setter> 123 </property> 124 125 <property name="linksOnTop"> 126 <setter> 127 <![CDATA[ 128 if(val) { 129 var container = this._id('links-container'); 130 var parent = container.parentNode; 131 var sib = container.nextSibling; 132 while (parent.firstChild !== container) { 133 parent.insertBefore(parent.removeChild(parent.firstChild), sib); 134 } 135 } 136 ]]> 137 </setter> 138 </property> 139 140 <property name="note" 141 onget="Zotero.debug('Getting note with .note deprecated -- use .item in zoteronoteeditor'); return this._item" 142 onset="Zotero.debug('Setting note with .note deprecated -- use .item in zoteronoteeditor'); this.item = val"/> 143 <property name="ref" onget="return this._item" onset="this.item = val"/> 144 145 <field name="collection"/> 146 147 <property name="noteField" onget="return this._id('noteField')" readonly="true"/> 148 <property name="value" onget="return this._id('noteField').value;" onset="this._id('noteField').value = val;"/> 149 150 <constructor> 151 <![CDATA[ 152 this.instanceID = Zotero.Utilities.randomString(); 153 this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'noteeditor'); 154 ]]> 155 </constructor> 156 157 <destructor> 158 <![CDATA[ 159 Zotero.Notifier.unregisterObserver(this._notifierID); 160 ]]> 161 </destructor> 162 163 <method name="notify"> 164 <parameter name="event"/> 165 <parameter name="type"/> 166 <parameter name="ids"/> 167 <parameter name="extraData"/> 168 <body><![CDATA[ 169 if (event != 'modify' || !this.item || !this.item.id) return; 170 for (let i = 0; i < ids.length; i++) { 171 let id = ids[i]; 172 if (id != this.item.id) { 173 continue; 174 } 175 if (extraData && extraData[id] && extraData[id].noteEditorID == this.instanceID) { 176 //Zotero.debug("Skipping notification from current note field"); 177 continue; 178 } 179 if (this.noteField.changed) { 180 //Zotero.debug("Note has changed since last save -- skipping refresh"); 181 return; 182 } 183 this.refresh(); 184 break; 185 } 186 ]]></body> 187 </method> 188 189 <method name="refresh"> 190 <body><![CDATA[ 191 Zotero.debug('Refreshing note editor'); 192 193 var textbox = this.noteField; 194 var textboxReadOnly = this._id('noteFieldReadOnly'); 195 var button = this._id('goButton'); 196 197 if (this.editable) { 198 textbox.hidden = false; 199 textboxReadOnly.hidden = true; 200 } 201 else { 202 textbox.hidden = true; 203 textboxReadOnly.hidden = false; 204 textbox = textboxReadOnly; 205 } 206 207 //var scrollPos = textbox.inputField.scrollTop; 208 if (this.item) { 209 // For sanity check in save() 210 textbox.setAttribute('itemID', this.item.id); 211 textbox.value = this.item.getNote(); 212 } 213 else { 214 textbox.value = ''; 215 textbox.removeAttribute('itemID'); 216 } 217 //textbox.inputField.scrollTop = scrollPos; 218 219 this._id('links-container').hidden = !(this.displayTags && this.displayRelated); 220 this._id('links-box').refresh(); 221 222 if (this.keyDownHandler) { 223 textbox.setAttribute('onkeydown', 224 'document.getBindingParent(this).handleKeyDown(event)'); 225 } 226 else { 227 textbox.removeAttribute('onkeydown'); 228 } 229 230 if (this.commandHandler) { 231 textbox.setAttribute('oncommand', 232 'document.getBindingParent(this).commandHandler()'); 233 } 234 else { 235 textbox.removeAttribute('oncommand'); 236 } 237 238 if (this.displayButton) { 239 button.label = this.buttonCaption; 240 button.hidden = false; 241 button.setAttribute('oncommand', 242 'document.getBindingParent(this).clickHandler(this)'); 243 } 244 else { 245 button.hidden = true; 246 } 247 ]]></body> 248 </method> 249 250 <method name="save"> 251 <body><![CDATA[ 252 return Zotero.spawn(function* () { 253 try { 254 if (this._mode == 'view') { 255 Zotero.debug("Not saving read-only note"); 256 return; 257 } 258 259 var noteField = this._id('noteField'); 260 var value = noteField.value; 261 if (value === null) { 262 Zotero.debug("Note value not available -- not saving", 2); 263 return; 264 } 265 266 // Update note 267 if (this.item) { 268 // If note field doesn't match item, abort save and run error handler 269 if (noteField.getAttribute('itemID') != this.item.id) { 270 throw new Error("Note field doesn't match current item"); 271 } 272 273 let changed = this.item.setNote(value); 274 if (changed && this.saveOnEdit) { 275 this.noteField.changed = false; 276 yield this.item.saveTx({ 277 notifierData: { 278 noteEditorID: this.instanceID 279 } 280 }); 281 } 282 return; 283 } 284 285 // Create new note 286 var item = new Zotero.Item('note'); 287 if (this.parentItem) { 288 item.libraryID = this.parentItem.libraryID; 289 } 290 item.setNote(value); 291 if (this.parentItem) { 292 item.parentKey = this.parentItem.key; 293 } 294 if (this.saveOnEdit) { 295 var id = yield item.saveTx(); 296 297 if (!this.parentItem && this.collection) { 298 this.collection.addItem(id); 299 } 300 } 301 302 this.item = item; 303 } 304 catch (e) { 305 Zotero.logError(e); 306 307 if (this.hasAttribute('onerror')) { 308 let fn = new Function("", this.getAttribute('onerror')); 309 fn.call(this) 310 } 311 if (this.onError) { 312 this.onError(e); 313 } 314 } 315 }.bind(this)); 316 ]]></body> 317 </method> 318 319 <!-- Used to insert a tab manually --> 320 <method name="handleKeyDown"> 321 <parameter name="event"/> 322 <body> 323 <![CDATA[ 324 switch (event.keyCode) { 325 case 9: 326 if (event.ctrlKey || event.altKey) { 327 return; 328 } 329 330 event.stopPropagation(); 331 event.preventDefault(); 332 333 // On shift-tab, focus the element specified in 334 // the 'previousfocus' attribute 335 if (event.shiftKey) { 336 let id = this.getAttribute('previousfocus'); 337 if (id) { 338 setTimeout(function () { 339 document.getElementById(id).focus(); 340 }, 0); 341 } 342 return; 343 } 344 345 // Insert tab manually 346 // 347 // From http://kb.mozillazine.org/Inserting_text_at_cursor 348 try { 349 var command = "cmd_insertText"; 350 var controller = document.commandDispatcher.getControllerForCommand(command); 351 if (controller && controller.isCommandEnabled(command)) { 352 controller = controller.QueryInterface(Components.interfaces.nsICommandController); 353 var params = Components.classes["@mozilla.org/embedcomp/command-params;1"] 354 .createInstance(Components.interfaces.nsICommandParams); 355 params.setStringValue("state_data", "\t"); 356 controller.doCommandWithParams(command, params); 357 } 358 } 359 catch (e) { 360 Zotero.debug("Can't do cmd_insertText!\n" + e, 1); 361 } 362 363 // DEBUG: is there a better way to prevent blur()? 364 setTimeout(function() { event.target.focus(); }, 1); 365 break; 366 } 367 ]]> 368 </body> 369 </method> 370 371 <method name="focus"> 372 <body> 373 <![CDATA[ 374 this._id('noteField').focus(); 375 ]]> 376 </body> 377 </method> 378 379 <method name="clearUndo"> 380 <body> 381 <![CDATA[ 382 this._id('noteField').clearUndo(); 383 ]]> 384 </body> 385 </method> 386 387 <method name="_id"> 388 <parameter name="id"/> 389 <body> 390 <![CDATA[ 391 return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0]; 392 ]]> 393 </body> 394 </method> 395 </implementation> 396 397 <content> 398 <xul:vbox xbl:inherits="flex"> 399 <xul:textbox id="noteField" type="styled" mode="note" 400 timeout="1000" flex="1" hidden="true"/> 401 <xul:textbox id="noteFieldReadOnly" type="styled" mode="note" 402 readonly="true" flex="1" hidden="true"/> 403 <xul:hbox id="links-container" hidden="true"> 404 <xul:linksbox id="links-box" flex="1" xbl:inherits="notitle"/> 405 </xul:hbox> 406 <xul:button id="goButton" hidden="true"/> 407 </xul:vbox> 408 </content> 409 </binding> 410 411 412 <binding id="links-box"> 413 <implementation> 414 <field name="itemRef"/> 415 <property name="item" onget="return this.itemRef;"> 416 <setter> 417 <![CDATA[ 418 this.itemRef = val; 419 420 this.id('tags').item = this.item; 421 this.id('related').item = this.item; 422 this.refresh(); 423 ]]> 424 </setter> 425 </property> 426 <property name="mode"> 427 <setter> 428 <![CDATA[ 429 this.id('related').mode = val; 430 this.id('tags').mode = val; 431 ]]> 432 </setter> 433 </property> 434 <field name="_parentItem"/> 435 <property name="parentItem" onget="return this._parentItem;"> 436 <setter> 437 <![CDATA[ 438 this._parentItem = val; 439 440 var parentText = this.id('parentText'); 441 if (parentText.firstChild) { 442 parentText.removeChild(parentText.firstChild); 443 } 444 445 if (this._parentItem && this.getAttribute('notitle') != '1') { 446 this.id('parent-row').hidden = undefined; 447 this.id('parentLabel').value = Zotero.getString('pane.item.parentItem'); 448 parentText.appendChild(document.createTextNode(this._parentItem.getDisplayTitle(true))); 449 } 450 ]]> 451 </setter> 452 </property> 453 <method name="tagsClick"> 454 <body><![CDATA[ 455 this.id('tags').reload(); 456 var x = this.boxObject.screenX; 457 var y = this.boxObject.screenY; 458 this.id('tagsPopup').openPopupAtScreen(x, y, false); 459 460 // If editable and no existing tags, open new empty row 461 var tagsBox = this.id('tags'); 462 if (tagsBox.mode == 'edit' && tagsBox.count == 0) { 463 this.id('tags').newTag(); 464 } 465 ]]></body> 466 </method> 467 468 <method name="refresh"> 469 <body><![CDATA[ 470 this.updateTagsSummary(); 471 this.updateRelatedSummary(); 472 ]]></body> 473 </method> 474 475 <method name="updateTagsSummary"> 476 <body><![CDATA[ 477 var v = this.id('tags').summary; 478 479 if (!v || v == "") { 480 v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]"; 481 } 482 483 this.id('tagsLabel').value = Zotero.getString('itemFields.tags') 484 + Zotero.getString('punctuation.colon'); 485 this.id('tagsClick').value = v; 486 ]]></body> 487 </method> 488 <method name="relatedClick"> 489 <body><![CDATA[ 490 var relatedList = this.item.relatedItems; 491 if (relatedList.length > 0) { 492 var x = this.boxObject.screenX; 493 var y = this.boxObject.screenY; 494 this.id('relatedPopup').openPopupAtScreen(x, y, false); 495 } 496 else { 497 this.id('related').add(); 498 } 499 ]]></body> 500 </method> 501 <method name="updateRelatedSummary"> 502 <body><![CDATA[ 503 var v = this.id('related').summary; 504 505 if (!v || v == "") { 506 v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]"; 507 } 508 509 this.id('relatedLabel').value = Zotero.getString('itemFields.related') 510 + Zotero.getString('punctuation.colon'); 511 this.id('relatedClick').value = v; 512 ]]></body> 513 </method> 514 <method name="parentClick"> 515 <body> 516 <![CDATA[ 517 if (!this.item || !this.item.id) { 518 return; 519 } 520 521 if (document.getElementById('zotero-pane')) { 522 var zp = ZoteroPane; 523 } 524 else { 525 var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] 526 .getService(Components.interfaces.nsIWindowMediator); 527 528 var lastWin = wm.getMostRecentWindow("navigator:browser"); 529 530 if (!lastWin) { 531 var lastWin = window.open(); 532 } 533 534 if (lastWin.ZoteroOverlay && !lastWin.ZoteroPane.isShowing()) { 535 lastWin.ZoteroOverlay.toggleDisplay(true); 536 } 537 538 var zp = lastWin.ZoteroPane; 539 } 540 541 Zotero.spawn(function* () { 542 var parentID = this.item.parentID; 543 yield zp.clearQuicksearch(); 544 zp.selectItem(parentID); 545 }, this); 546 ]]> 547 </body> 548 </method> 549 <method name="id"> 550 <parameter name="id"/> 551 <body> 552 <![CDATA[ 553 return document.getAnonymousNodes(this)[0].getElementsByAttribute('id',id)[0]; 554 ]]> 555 </body> 556 </method> 557 </implementation> 558 <content> 559 <xul:vbox xbl:inherits="flex"> 560 <xul:grid> 561 <xul:columns> 562 <xul:column/> 563 <xul:column flex="1"/> 564 </xul:columns> 565 <xul:rows> 566 <xul:row id="parent-row" hidden="true"> 567 <xul:label id="parentLabel"/> 568 <xul:label id="parentText" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).parentClick();"/> 569 </xul:row> 570 <xul:row> 571 <xul:label id="relatedLabel"/> 572 <xul:label id="relatedClick" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).relatedClick();"/> 573 </xul:row> 574 <xul:row> 575 <xul:label id="tagsLabel"/> 576 <xul:label id="tagsClick" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).tagsClick();"/> 577 </xul:row> 578 </xul:rows> 579 </xul:grid> 580 <xul:popupset> 581 <xul:menupopup id="relatedPopup" width="300" onpopupshowing="this.firstChild.refresh();"> 582 <xul:relatedbox id="related" flex="1"/> 583 </xul:menupopup> 584 <!-- The onpopup* stuff is an ugly hack to keep track of when the 585 popup is open (and not the descendent autocomplete popup, which also 586 seems to get triggered by these events for reasons that are less than 587 clear) so that we can manually refresh the popup if it's open after 588 autocomplete is used to prevent it from becoming unresponsive 589 590 Note: Code in tagsbox.xml is dependent on the DOM path between the 591 tagsbox and tagsLabel above, so be sure to update fixPopup() if it changes 592 --> 593 <xul:menupopup id="tagsPopup" ignorekeys="true" 594 onpopupshown="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ /* DEBUG: it would be nice to make this work -- if (this.firstChild.count==0){ this.firstChild.newTag(); } */ this.setAttribute('showing', 'true'); }" 595 onpopuphidden="if (!document.commandDispatcher.focusedElement || document.commandDispatcher.focusedElement.tagName=='xul:label'){ this.setAttribute('showing', 'false'); }"> 596 <xul:tagsbox id="tags" flex="1" mode="edit"/> 597 </xul:menupopup> 598 </xul:popupset> 599 </xul:vbox> 600 </content> 601 </binding> 602 </bindings>