Skip to content

Instantly share code, notes, and snippets.

@FND
Created June 1, 2009 12:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save FND/121370 to your computer and use it in GitHub Desktop.
Save FND/121370 to your computer and use it in GitHub Desktop.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Cork - virtual pinboard</title>
<link rel="stylesheet" type="text/css" href="styles/main.css">
<link rel="stylesheet" type="text/css" href="styles/jquery.ui.css">
</head>
<body>
<div id="pinboard" class="board">
<div class="toolbar"></div>
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js" type="text/javascript"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js" type="text/javascript"></script>
<script src="scripts/lib/jquery-json.js" type="text/javascript"></script>
<script src="scripts/main.js" type="text/javascript"></script>
<script src="scripts/util.js" type="text/javascript"></script>
<script src="scripts/pinboard.js" type="text/javascript"></script>
<script src="scripts/backends/tiddlyweb.js" type="text/javascript"></script>
</body>
</html>
(function($){function toIntegersAtLease(n)
{return n<10?'0'+n:n;}
Date.prototype.toJSON=function(date)
{return this.getUTCFullYear()+'-'+
toIntegersAtLease(this.getUTCMonth())+'-'+
toIntegersAtLease(this.getUTCDate());};var escapeable=/["\\\x00-\x1f\x7f-\x9f]/g;var meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};$.quoteString=function(string)
{if(escapeable.test(string))
{return'"'+string.replace(escapeable,function(a)
{var c=meta[a];if(typeof c==='string'){return c;}
c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);})+'"';}
return'"'+string+'"';};$.toJSON=function(o,compact)
{var type=typeof(o);if(type=="undefined")
return"undefined";else if(type=="number"||type=="boolean")
return o+"";else if(o===null)
return"null";if(type=="string")
{return $.quoteString(o);}
if(type=="object"&&typeof o.toJSON=="function")
return o.toJSON(compact);if(type!="function"&&typeof(o.length)=="number")
{var ret=[];for(var i=0;i<o.length;i++){ret.push($.toJSON(o[i],compact));}
if(compact)
return"["+ret.join(",")+"]";else
return"["+ret.join(", ")+"]";}
if(type=="function"){throw new TypeError("Unable to convert object of type 'function' to json.");}
var ret=[];for(var k in o){var name;type=typeof(k);if(type=="number")
name='"'+k+'"';else if(type=="string")
name=$.quoteString(k);else
continue;var val=$.toJSON(o[k],compact);if(typeof(val)!="string"){continue;}
if(compact)
ret.push(name+":"+val);else
ret.push(name+": "+val);}
return"{"+ret.join(", ")+"}";};$.compactJSON=function(o)
{return $.toJSON(o,true);};$.evalJSON=function(src)
{return eval("("+src+")");};$.secureEvalJSON=function(src)
{var filtered=src;filtered=filtered.replace(/\\["\\\/bfnrtu]/g,'@');filtered=filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']');filtered=filtered.replace(/(?:^|:|,)(?:\s*\[)+/g,'');if(/^[\],:{}\s]*$/.test(filtered))
return eval("("+src+")");else
throw new SyntaxError("Error parsing JSON, source is not valid.");};})(jQuery);
.ui-resizable { position: relative; }
.ui-resizable-handle { position: absolute; font-size: 0.1px; z-index: 99999; display: block; }
.ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; }
.ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0px; }
.ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0px; }
.ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0px; height: 100%; }
.ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0px; height: 100%; }
.ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; }
.ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; }
.ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; }
.ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px; }
.ui-draggable { cursor: move; }
* {
margin: 0;
padding: 0;
}
html,
body,
.board {
height: 100%;
}
.board {
position: relative;
background-color: #FFE;
}
.snippet {
width: 300px;
height: 200px;
overflow: hidden;
border: 1px solid #AAA;
padding: 5px;
background-color: #EEF;
}
jQuery.noConflict();
jQuery(function() {
var $ = jQuery; // alias
var container = $("#pinboard");
var pb = new Pinboard(container);
$(document).trigger("startup", { pinboard: pb }); // XXX: rename event!?
});
(function($) {
/* Pinboard object */
Pinboard = function(container) {
this.snippets = []; // XXX: redundant? (cf. Pinboard.snippets - also, YAGNI)
this.container = container;
this.render(container);
return this;
};
Pinboard.snippets = {}; // required to bridge DOM events -- XXX: ugly hack?
Pinboard.prototype.add = function(snippet) {
this.snippets.push(snippet);
Pinboard.snippets[snippet.id] = snippet; // XXX: renders ID attribute redundant!?
snippet.render(this.container);
};
Pinboard.prototype.render = function(container) {
var self = this;
// populate toolbar
var createNewSnippet = function(ev) {
var snippet = new Snippet();
self.add(snippet);
};
$('<input type="button" value="new">').
click(createNewSnippet).
appendTo("#pinboard .toolbar");
// render snippets -- XXX: snippets always empty on startup!?
for(var i = 0; i < this.snippets.length; i++) {
this.snippets[i].render(container);
}
};
/* Snippet object */
Snippet = function(attr) { // XXX: rename "attr"
attr = attr || {};
this.id = attr.id || _generateId();
this.caption = attr.caption || this.id;
this.content = attr.content || "N/A";
this.dimensions = attr.dimensions || {}; // XXX: separate size and position?
return this;
};
Snippet.prototype.render = function(container) {
var el = $('<li class="snippet" />').attr("id", this.id);
$("<h3 />").text(this.caption).editable().appendTo(el);
$("<p />").text(this.content).editable({ multiLine: true, autosize: true }).
appendTo(el);
var dim = this.dimensions; // alias -- XXX: unnecessary?
if(dim.h && dim.v) {
el.css({ width: dim.h, height: dim.v });
}
if(dim.x && dim.y) {
el.css({ position: "absolute", left: dim.x, top: dim.y });
}
var resizeCallback = function(ev, ui) {
$(this).trigger("edit", { type: "resize", size: ui.size });
};
var dragCallback = function(ev, ui) {
$(this).trigger("edit", { type: "drag", position: ui.position });
};
$(el).resizable({ stop: resizeCallback }).draggable({ stop: dragCallback }).
appendTo(container);
};
/* event handling */
$(document).bind("edit", function(ev, data) {
var snippetEl = _findContainingSnippet(ev.target);
var snippet = _resolveSnippet(snippetEl);
switch(data.type) {
case "content":
snippet.caption = $("h3", snippetEl).text();
snippet.content = $("p", snippetEl).text();
break;
case "resize":
snippet.dimensions.h = data.size.width;
snippet.dimensions.v = data.size.height;
break;
case "drag":
var pos = $(snippetEl).position();
snippet.dimensions.x = pos.left;
snippet.dimensions.y = pos.top;
break;
default:
break;
}
$(document).trigger("snippetEdit", { type: data.type, snippet: snippet }); // XXX: rename event
});
/* utilities */
// retrieve DOM element representing snippet
var _findContainingSnippet = function(el) { // XXX: rename; should be Pinboard method!?
return $(el).hasClass(".snippet") ? el : $(el).parent(".snippet")[0];
};
// determine Snippet instance from jQuery object
var _resolveSnippet = function(el) { // XXX: rename; should be Pinboard method!?
var id = $(el).attr("id");
return Pinboard.snippets[id];
};
// generate pseudo-unique ID
var _generateId = function() {
return parseInt(Math.random() * new Date().valueOf(), 10); // XXX: safe enough?
};
})(jQuery);
(function($) {
// settings -- XXX: hardcoded
var host = "http://localhost:8080";
var recipe = "default";
// event subscriptions
$(document).bind("snippetEdit", function(ev, data) {
saveSnippet(data.snippet);
});
$(document).bind("startup", function(ev, data) {
loadSnippets(recipe, data.pinboard);
});
// utility functions -- XXX: should not all be private!?
var loadSnippets = function(recipe, pinboard) {
var uri = generateURI(host, recipe);
var callback = function(xhr, statusText) {
if(xhr.status == 200 || xhr.status == 304) {
var json = $.evalJSON(xhr.responseText); // XXX: obsolete when using success callback!?
$.each(json, function(i, item) {
loadSnippet(item.title, pinboard);
});
} else {
console.log(statusText, xhr.responseText, xhr); // XXX: should be UI notification
}
};
$.localAjax({
url: uri,
type: "GET",
dataType: "json",
complete: callback // XXX: use success instead!?
});
};
var loadSnippet = function(id, pinboard) {
var uri = generateURI(host, recipe, id);
var callback = function(xhr, statusText) {
if(xhr.status == 200 || xhr.status == 304) {
var json = $.evalJSON(xhr.responseText); // XXX: obsolete when using success callback!?
var snippet = internalize(json);
pinboard.add(snippet);
}
};
$.localAjax({
url: uri,
type: "GET",
dataType: "json",
complete: callback // XXX: use success instead!?
});
};
var saveSnippet = function(snippet) {
var uri = generateURI(host, recipe, snippet.id);
$.localAjax({
url: uri,
type: "PUT",
contentType: "application/json",
data: externalize(snippet),
complete: console.log // DEBUG
});
};
var externalize = function(snippet) { // XXX: rename
var tiddler = {
fields: {}
};
for(var key in snippet) {
switch(key) {
case "id": // title in URL
break;
case "content":
tiddler.text = snippet[key];
break;
default:
if(snippet.hasOwnProperty(key)) {
tiddler.fields[key] = snippet[key];
}
break;
}
}
tiddler.fields = flattenJSON(tiddler.fields); // XXX: hacky workaround
for(key in tiddler.fields) {
tiddler.fields[key] = tiddler.fields[key].toString(); // XXX: hacky workaround
}
return $.toJSON(tiddler);
};
var internalize = function(json) {
var snippet = new Snippet({
id: json.title,
caption: json.fields.caption,
content: json.text,
dimensions: { // XXX: not always present - leads to NaN
h: parseInt(json.fields["dimensions.h"], 10),
v: parseInt(json.fields["dimensions.v"], 10),
x: parseInt(json.fields["dimensions.x"], 10),
y: parseInt(json.fields["dimensions.y"], 10)
}
});
// TODO: revision, created, modified, bag, modifier
return snippet;
};
var generateURI = function(host, recipe, title) { // XXX: not very generic at this point
var uri = host + "/recipes/" + encodeURIComponent(recipe) + "/tiddlers"; // XXX: doesn't work with server_prefix!?
if(title) {
uri += "/" + encodeURIComponent(title);
}
return uri;
};
})(jQuery);
(function($) {
/*
* enable editing functionality on element
*
* Switching from edit to view mode triggers "edit" event.
*
* options argument members
* multiLine: multi-line content editing (defaults to false)
* autosize: automatically resize input box (defaults to false)
* editTrigger: event type(s) triggering edit mode (defaults to "dblclick")
* viewTrigger: event type(s) triggering view mode (defaults to "blur")
*/
$.fn.editable = function(options) {
options = options || {};
var editTrigger = options.editTrigger || "dblclick";
var viewTrigger = options.viewTrigger || "blur";
return this.bind(editTrigger, function(ev) {
$(this)._edit(viewTrigger, options.multiLine, options.autosize);
});
};
$.fn._edit = function(trigger, multiLine, autosize) { // TODO: combine arguments -- XXX: rename method to avoid namespace polution?
var oldTag = this[0].tagName;
var newTag = multiLine ? "textarea" : "input";
var width = this.width() - 5; // XXX: "padding" hardcoded
var height = multiLine ? this.height() : null; // XXX: does not compensate for scroll bars
var triggerCallback = function(ev) {
$(this)._render(oldTag).
editable({ multiLine: multiLine, viewTrigger: trigger }). // XXX: original element's events are lost
trigger("edit", { type: "content" }); // XXX: rename event!?
};
var el = $("<" + newTag + "/>").val(this.text()).
width(width).height(height).bind(trigger, triggerCallback).
replaceAll(this);
if(autosize) { // DEBUG: inverted for testing purposes
el.keypress(function(ev) { $(this)._autosize(); });
}
return el;
};
$.fn._render = function(nodeType) { // XXX: rename method to avoid namespace polution?
nodeType = nodeType || "div";
return $("<" + nodeType + "/>").text(this.val()).replaceAll(this);
};
// XXX: feature creep!?
// TODO: optional maxWidth/maxHeight
$.fn._autosize = function(horizontal) { // XXX: rename method to avoid namespace polution?
if(horizontal) {
this.width(this[0].scrollWidth);
} else {
this.height(this[0].scrollHeight);
}
return this;
};
/*
* enable AJAX calls from a local file
* triggers regular $.ajax call after requesting enhanced privileges
*/
$.localAjax = function(args) {
if(document.location.protocol.indexOf("http") == -1 && window.Components &&
window.netscape && window.netscape.security) {
window.netscape.security.PrivilegeManager.
enablePrivilege("UniversalBrowserRead");
}
return $.ajax(args);
};
flattenJSON = function(obj, options) {
options = options || {};
var target = options.target || {};
var prefix = options.prefix || "";
for(var key in obj) {
if(obj.hasOwnProperty(key)) {
var prop = obj[key];
var exclude = prop instanceof Array || prop instanceof RegExp ||
prop instanceof Date;
if(typeof prop === "object" && !exclude) {
flattenJSON(prop, {
target: target,
prefix: prefix + key + "."
});
} else {
target[prefix + key] = prop;
}
}
}
return target;
};
})(jQuery);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment