2 * A Cache object is used to manage the node and the file behind it.
4 * @param {Object} node A DOM Node to watch.
6 function CacheObj(node) {
9 /* Gumdrop Image URL */
10 that.gumdrop_url = 'chrome://itsalltext/locale/gumdrop.png';
11 /* Gumdrop Image Width */
12 that.gumdrop_width = ItsAllText.localeString('gumdrop.width');
13 /* Gumdrop Image Height */
14 that.gumdrop_height = ItsAllText.localeString('gumdrop.height');
20 that.initial_background = '';
21 that._is_watching = false;
23 that.node_id = that.getNodeIdentifier(node);
24 var doc = node.ownerDocument;
26 /* This is a unique identifier for use on the web page to prevent the
27 * web page from knowing what it's connected to.
30 that.uid = that.hashString([ doc.location.toString(),
32 that.node_id ].join(':'));
33 // @todo [security] Add a serial to the uid hash.
35 node.setAttribute(ItsAllText.MYSTRING+'_UID', that.uid);
36 ItsAllText.tracker[that.uid] = that;
38 /* Figure out where we will store the file. While the filename can
39 * change, the directory that the file is stored in should not!
41 var host = window.escape(doc.location.hostname);
42 var hash = that.hashString([ doc.location.protocol,
45 doc.location.pathname,
46 that.node_id ].join(':'));
47 that.base_filename = [host, hash.slice(0,10)].join('.');
48 /* The current extension.
51 that.extension = null;
53 /* Stores an nsILocalFile pointing to the current filename.
58 /* Set the default extension and create the nsIFile object. */
59 var extension = node.getAttribute('itsalltext-extension');
60 if (typeof(extension) != 'string' || !extension.match(/^[.a-z0-9]+$/i)) {
61 extension = ItsAllText.getExtensions()[0];
63 that.setExtension(extension);
65 that.initFromExistingFile();
68 * A callback for when the textarea/textbox or button has
69 * the mouse waved over it.
70 * @param {Event} event The event object.
72 that.mouseover = function(event) {
73 var style = that.button?that.button.style:null;
75 style.setProperty('opacity', '0.7', 'important');
76 ItsAllText.refreshTextarea(that.node);
81 * A callback for when the textarea/textbox or button has
82 * the mouse waved over it and the moved off.
83 * @param {Event} event The event object.
85 that.mouseout = function(event) {
86 var style = that.button?that.button.style:null;
88 style.setProperty('opacity', '0.1', 'important');
94 * Set the extension for the file to ext.
95 * @param {String} ext The extension. Must include the dot. Example: .txt
97 CacheObj.prototype.setExtension = function(ext) {
98 if (ext == this.extension) {
99 return; /* It's already set. No problem. */
102 /* Create the nsIFile object */
103 var file = ItsAllText.factoryFile();
104 file.initWithFile(ItsAllText.getEditDir());
105 file.append([this.base_filename,ext].join(''));
107 this.extension = ext;
112 * This function looks for an existing file and starts to monitor
113 * if the file exists already. It also deletes all existing files for
116 CacheObj.prototype.initFromExistingFile = function() {
117 var base = this.base_filename;
118 var fobj = ItsAllText.getEditDir();
119 var entries = fobj.directoryEntries;
121 var tmpfiles = /(\.bak|.tmp|~)$/;
123 while (entries.hasMoreElements()) {
124 entry = entries.getNext();
125 entry.QueryInterface(Components.interfaces.nsIFile);
126 if (entry.leafName.indexOf(base) === 0) {
128 if (ext === null && !entry.leafName.match(tmpfiles)) {
129 ext = entry.leafName.slice(base.length);
134 that.debug('unable to remove',entry,'because:',e);
139 this.setExtension(ext);
140 this._is_watching = true;
145 * Returns a unique identifier for the node, within the document.
146 * @returns {String} the unique identifier.
148 CacheObj.prototype.getNodeIdentifier = function(node) {
149 var id = node.getAttribute('id');
150 var name, doc, attr, serial;
152 name = node.getAttribute('name');
153 doc = node.ownerDocument.getElementsByTagName('html')[0];
154 attr = ItsAllText.MYSTRING+'_id_serial';
156 /* Get a serial that's unique to this document */
157 serial = doc.getAttribute(attr);
158 if (serial) { serial = parseInt(serial, 10)+1;
159 } else { serial = 1; }
160 id = [ItsAllText.MYSTRING,'generated_id',name,serial].join('_');
161 doc.setAttribute(attr,serial);
162 node.setAttribute('id',id);
168 * Convert to this object to a useful string.
169 * @returns {String} A string representation of this object.
171 CacheObj.prototype.toString = function() {
174 " timestamp=",this.timestamp,
180 * Write out the contents of the node.
182 CacheObj.prototype.write = function() {
183 var foStream = Components.
184 classes["@mozilla.org/network/file-output-stream;1"].
185 createInstance(Components.interfaces.nsIFileOutputStream);
187 /* write, create, truncate */
188 foStream.init(this.file, 0x02 | 0x08 | 0x20,
189 parseInt('0600',8), 0);
191 /* We convert to charset */
192 var conv = Components.
193 classes["@mozilla.org/intl/scriptableunicodeconverter"].
194 createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
195 conv.charset = ItsAllText.getCharset();
197 var text = conv.ConvertFromUnicode(this.node.value);
198 foStream.write(text, text.length);
201 /* Reset Timestamp and filesize, to prevent a spurious refresh */
202 this.timestamp = this.file.lastModifiedTime;
203 this.size = this.file.fileSize;
205 /* Register the file to be deleted on app exit. */
206 Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"].
207 getService(Components.interfaces.nsPIExternalAppLauncher).
208 deleteTemporaryFileOnExit(this.file);
210 return this.file.path;
214 * Fetches the computed CSS attribute for a specific node
215 * @param {DOM} node The DOM node to get the information for.
216 * @param {String} attr The CSS-style attribute to fetch (not DOM name).
219 CacheObj.prototype.getStyle = function(node, attr) {
220 var view = node ? node.ownerDocument.defaultView : null;
221 var style = view.getComputedStyle(node, '');
222 return style.getPropertyCSSValue(attr).cssText;
225 // @todo [9] IDEA: Pass in the line number to the editor, arbitrary command?
226 // @todo [9] IDEA: Allow the user to pick an alternative editor?
227 // @todo [9] IDEA: A different editor per extension?
229 * Edit a textarea as a file.
230 * @param {String} extension The extension of the file to edit.
232 CacheObj.prototype.edit = function(extension) {
233 if (typeof(extension) == 'string') {
234 this.setExtension(extension);
236 var filename = this.write();
237 this.initial_background = this.node.style.backgroundColor;
238 this.initial_color = this.node.style.color;
241 var args, result, ec, e, params;
244 program = ItsAllText.getEditor();
246 if (program === null) { throw {name:"Editor is not set."}; }
247 if (!program.exists()) { throw {name:"NS_ERROR_FILE_NOT_FOUND"}; }
248 /* Mac check because of
249 * https://bugzilla.mozilla.org/show_bug.cgi?id=322865 */
250 if (!(ItsAllText.isDarwin() || program.isExecutable())) {
251 throw {name:"NS_ERROR_FILE_ACCESS_DENIED"}; }
253 // create an nsIProcess
254 process = Components.
255 classes["@mozilla.org/process/util;1"].
256 createInstance(Components.interfaces.nsIProcess);
257 process.init(program);
260 // If first param is true, calling thread will be blocked until
261 // called process terminates.
262 // Second and third params are used to pass command-line arguments
266 ec = process.run(false, args, args.length, result);
267 this._is_watching = true;
270 exists: program ? program.exists() : false,
271 path: ItsAllText.preferences.editor,
273 window.openDialog('chrome://itsalltext/chrome/badeditor.xul',
275 "chrome,titlebar,toolbar,centerscreen,modal",
277 if(params.out !== null && params.out.do_preferences) {
278 ItsAllText.openPreferences(true);
279 this.edit(extension);
285 * Delete the file from disk.
287 CacheObj.prototype.remove = function() {
288 if(this.file.exists()) {
292 that.debug('remove(',this.file.path,'): ',e);
300 * Read the file from disk.
302 CacheObj.prototype.read = function() {
303 /* read file, reset ts & size */
304 var DEFAULT_REPLACEMENT_CHARACTER = 65533;
306 var fis, istream, str, e;
310 classes["@mozilla.org/network/file-input-stream;1"].
311 createInstance(Components.interfaces.nsIFileInputStream);
312 fis.init(this.file, 0x01, parseInt('00400',8), 0);
313 // MODE_RDONLY | PERM_IRUSR
315 istream = Components.
316 classes["@mozilla.org/intl/converter-input-stream;1"].
317 createInstance(Components.interfaces.nsIConverterInputStream);
318 istream.init(fis, ItsAllText.getCharset(), 4096, DEFAULT_REPLACEMENT_CHARACTER);
321 while (istream.readString(4096, str) !== 0) {
322 buffer.push(str.value);
328 this.timestamp = this.file.lastModifiedTime;
329 this.size = this.file.fileSize;
331 return buffer.join('');
338 * Has the file object changed?
339 * @returns {boolean} returns true if the file has changed on disk.
341 CacheObj.prototype.hasChanged = function() {
342 /* Check exists. Check ts and size. */
343 if(!this._is_watching ||
344 !this.file.exists() ||
345 !this.file.isReadable() ||
346 (this.file.lastModifiedTime == this.timestamp &&
347 this.file.fileSize == this.size)) {
355 * Part of the fading technique.
356 * @param {Object} pallet A Color blend pallet object.
357 * @param {int} step Size of a step.
358 * @param {delay} delay Delay in microseconds.
360 CacheObj.prototype.fadeStep = function(background_pallet, color_pallet, step, delay) {
363 if (step < background_pallet.length) {
364 that.node.style.backgroundColor = background_pallet[step].hex();
365 that.node.style.color = color_pallet[step].hex();
367 setTimeout(that.fadeStep(background_pallet, color_pallet, step, delay),delay);
369 that.node.style.backgroundColor = that.initial_background;
370 that.node.style.color = that.initial_color;
376 * Node fade technique.
377 * @param {int} steps Number of steps in the transition.
378 * @param {int} delay How long to wait between delay (microseconds).
380 CacheObj.prototype.fade = function(steps, delay) {
381 var color = this.getStyle(this.node, 'color');
382 var color_stop = new ItsAllText.Color(color);
383 var color_start = new ItsAllText.Color('black');
384 var color_pallet = color_start.blend(color_stop, steps);
386 var background = this.getStyle(this.node, 'background-color');
387 var background_stop = new ItsAllText.Color(background);
388 var background_start = new ItsAllText.Color('yellow');
389 var background_pallet = background_start.blend(background_stop, steps);
390 setTimeout(this.fadeStep(background_pallet, color_pallet, 0, delay), delay);
394 * Update the node from the file.
395 * @returns {boolean} Returns true ifthe file changed.
397 CacheObj.prototype.update = function() {
399 if (this.hasChanged()) {
401 if (value !== null) {
403 this.node.value = value;
407 return false; // If we fall through, we
411 * Add the gumdrop to a textarea.
412 * @param {Object} cache_object The Cache Object that contains the node.
414 CacheObj.prototype.addGumDrop = function() {
415 var cache_object = this;
416 if (cache_object.button !== null) {
417 cache_object.adjust();
418 return; /*already done*/
420 if (ItsAllText.getDisableGumdrops()) {
423 ItsAllText.debug('addGumDrop()',cache_object.node_id,cache_object.uid);
425 var node = cache_object.node;
426 var doc = node.ownerDocument;
427 var offsetNode = node;
428 if (!node.parentNode) { return; }
430 var gumdrop = doc.createElementNS(ItsAllText.XHTMLNS, "img");
431 gumdrop.setAttribute('src', this.gumdrop_url);
432 var gid = cache_object.getNodeIdentifier(gumdrop);
434 if (ItsAllText.getDebug()) {
435 gumdrop.setAttribute('title', cache_object.node_id);
437 gumdrop.setAttribute('title', ItsAllText.localeString('program_name'));
439 cache_object.button = gumdrop; // Store it for easy finding in the future.
442 gumdrop.style.setProperty('cursor', 'pointer', 'important');
443 gumdrop.style.setProperty('display', 'block', 'important');
444 gumdrop.style.setProperty('position', 'absolute', 'important');
445 gumdrop.style.setProperty('padding', '0', 'important');
446 gumdrop.style.setProperty('margin', '0', 'important');
447 gumdrop.style.setProperty('border', 'none', 'important');
448 gumdrop.style.setProperty('zIndex', '1', 'important'); // we want it just above normal items.
450 gumdrop.style.setProperty('width', this.gumdrop_width+'px', 'important');
451 gumdrop.style.setProperty('height', this.gumdrop_height+'px', 'important');
453 gumdrop.setAttribute(ItsAllText.MYSTRING+'_UID', cache_object.uid);
455 var clickfun = function(event) {
457 event.stopPropagation();
460 var contextfun = function(event) {
461 /* This took forever to fix; roughly 80+ man hours were spent
462 * over 5 months trying to make this stupid thing work.
463 * The documentation is completely wrong and useless.
465 * Excuse me while I scream.
467 * See Mozilla bugs: 287357, 362403, 279703
469 var popup = ItsAllText.rebuildMenu(cache_object.uid);
470 document.popupNode = popup;
471 popup.showPopup(popup,
472 event.screenX, event.screenY,
473 'context', null, null);
474 event.stopPropagation();
478 // Click event handler
479 gumdrop.addEventListener("click", clickfun, false);
480 gumdrop.addEventListener("contextmenu", contextfun, false);
482 // Insert it into the document
483 var parent = node.parentNode;
484 var nextSibling = node.nextSibling;
487 parent.insertBefore(gumdrop, nextSibling);
489 parent.appendChild(gumdrop);
492 // Add mouseovers/outs
493 node.addEventListener("mouseover", cache_object.mouseover, false);
494 node.addEventListener("mouseout", cache_object.mouseout, false);
495 gumdrop.addEventListener("mouseover", cache_object.mouseover, false);
496 gumdrop.addEventListener("mouseout", cache_object.mouseout, false);
498 cache_object.mouseout(null);
499 cache_object.adjust();
503 * Updates the position of the gumdrop, incase the textarea shifts around.
505 CacheObj.prototype.adjust = function() {
506 var gumdrop = this.button;
508 var doc = el.ownerDocument;
510 if (ItsAllText.getDisableGumdrops()) {
511 if(gumdrop && gumdrop.style.display != 'none') {
512 gumdrop.style.setProperty('display', 'none', 'important');
517 var style = gumdrop.style;
518 if (!gumdrop || !el) { return; }
520 var cstyle = doc.defaultView.getComputedStyle(el, '');
521 if (cstyle.display == 'none' ||
522 cstyle.visibility == 'hidden' ||
523 el.getAttribute('readonly') ||
524 el.getAttribute('disabled')
528 if (style.display != display) {
529 style.setProperty('display', display, 'important');
532 /* Reposition the gumdrops incase the dom changed. */
533 var left = Math.max(1, el.offsetWidth-this.gumdrop_width);
534 var top = el.offsetHeight;
536 if (el.offsetParent === gumdrop.offsetParent) {
537 left += el.offsetLeft;
540 coord = ItsAllText.getContainingBlockOffset(el, gumdrop.offsetParent);
545 left = [left,'px'].join('');
546 top = [top,'px'].join('');
547 if(style.left != left) { style.setProperty('left', left, 'important');}
548 if(style.top != top) { style.setProperty('top', top, 'important');}
553 * Creates a mostly unique hash of a string
554 * Most of this code is from:
555 * http://developer.mozilla.org/en/docs/nsICryptoHash
556 * @param {String} some_string The string to hash.
557 * @returns {String} a hashed string.
559 CacheObj.prototype.hashString = function(some_string) {
560 var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
561 converter.charset = "UTF-8";
563 /* result is the result of the hashing. It's not yet a string,
564 * that'll be in retval.
565 * result.value will contain the array length
569 /* data is an array of bytes */
570 var data = converter.convertToByteArray(some_string, result);
571 var ch = Components.classes["@mozilla.org/security/hash;1"].createInstance(Components.interfaces.nsICryptoHash);
574 ch.update(data, data.length);
575 var hash = ch.finish(true);
577 // return the two-digit hexadecimal code for a byte
578 var toHexString = function(charCode) {
579 return ("0" + charCode.toString(36)).slice(-2);
582 // convert the binary hash data to a hex string.
585 retval[i] = toHexString(hash.charCodeAt(i));
588 return(retval.join(""));