// Copyright 2007 Google Inc. // All Rights Reserved // // Author: Bret Taylor // A single task. We know how to draw ourselves in the DOM and support the // dragging/reordering operations on tasks. function Task(listKey, key, description, completed) { this.listKey_ = listKey; this.key_ = key; this.description_ = description; this.completed_ = completed; } // Returns this task's datastore key. Task.prototype.key = function() { return this.key_; } // Parses a list of tasks of the form: // // [[listKey, key, description, completed]*] // // Specifically, the list should be an array of arrays representing arguments // to the Task constructor. Task.parseList = function(list) { var result = []; function wrapper() {} wrapper.prototype = Task.prototype; for (var i = 0; i < list.length; i++) { var task = new wrapper(); Task.apply(task, list[i]); result.push(task); } return result; } // Attaches this task to the given element container, creating the DOM // elements representing this task. Task.prototype.attach = function(container) { var element = this.createElement_("div", container, "task"); if (this.completed_) { cssAddClass(element, "completed"); } element.style.position = "relative"; var table = this.createElement_("table", element); var tbody = this.createElement_("tbody", table); var tr = this.createElement_("tr", tbody); // Create the checkbox var checkboxCell = this.createElement_("td", tr, "checkbox"); var checkbox; try { checkbox = document.createElement(''); checkboxCell.appendChild(checkbox); } catch (e) { checkbox = this.createElement_("input", checkboxCell) checkbox.type = "checkbox"; } checkbox.name = "task"; checkbox.value = this.key_; checkbox.checked = this.completed_; Event.addListener(checkbox, "click", callback(this, this.onCheck_)); // Prevent dragging through the checkbox Event.addListener(checkbox, "mousedown", stopEvent); var descriptionCell = this.createElement_("td", tr, "description"); this.descriptionContainer_ = this.createElement_("div", descriptionCell); this.descriptionContainer_.style.position = "relative"; this.resetDescriptionText_(); // Enable drag positioning of this task var dragger = new Dragger(element, true); Event.addListener(dragger, "dragstart", callback(this, this.onDragStart_)); Event.addListener(dragger, "dragend", callback(this, this.onDragEnd_)); Event.addListener(dragger, "drag", callback(this, this.onDrag_)); Event.addListener(dragger, "click", callback(this, this.edit)); this.checkbox_ = checkbox; this.element_ = element; return element; } // Send the AJAX task completed request when our check state changes. Task.prototype.onCheck_ = function() { // Update the user interface if (this.checkbox_.checked) { cssAddClass(this.element_, "completed"); } else { cssRemoveClass(this.element_, "completed"); } // Serialize the change to the server var args = [ "id=" + encodeURIComponent(this.key_) ]; if (this.checkbox_.checked) { args.push("completed=1"); } download("/settaskcompleted.do", null, { post: true, body: args.join("&") }); } // Turns this task to edit mode, which replaces our text with a INPUT box // for editing the task description. We go back to view mode automatically // when the text box loses focus or the user hits Return/Escape. Task.prototype.edit = function() { // Only edit if we are not already editing if (this.textBox_) return; var textBox; try { textBox = document.createElement(''); } catch (e) { textBox = this.createElement_("input") textBox.type = "text"; } textBox.style.position = "absolute"; textBox.style.left = this.descriptionContainer_.offsetLeft - 3 + "px"; textBox.style.top = this.descriptionContainer_.offsetTop - 3 + "px"; textBox.style.width = this.descriptionContainer_.offsetWidth + "px"; textBox.style.border = "1px solid silver"; textBox.style.padding = "2px"; textBox.style.margin = "0"; textBox.style.zIndex = 1; textBox.value = this.description_; enableSelection(this.element_); Event.addListener(textBox, "keypress", callback(this, this.onEditKeyPress_)); Event.addListener(textBox, "blur", callback(this, this.saveEdit_)); Event.addListener(textBox, "mousedown", stopEvent); this.descriptionContainer_.parentNode.appendChild(textBox); this.descriptionContainer_.style.display = "none"; textBox.focus(); this.textBox_ = textBox; } // Check for Return/Escape key press events to stop editing. We save the // edit on Return, cancel the edit on escape. Task.prototype.onEditKeyPress_ = function(e) { if (e.keyCode == 13) { cancelEvent(e); this.saveEdit_(); } else if (e.keyCode == 27) { cancelEvent(e); this.cleanUpEdit_(); } } // Puts this task back into view mode from edit mode. Task.prototype.cleanUpEdit_ = function() { this.textBox_.parentNode.removeChild(this.textBox_); this.textBox_ = null; this.descriptionContainer_.style.display = ""; disableSelection(this.element_); } // Save the results of this edit to the server. Task.prototype.saveEdit_ = function() { var changed = this.textBox_.value != this.description_; if (changed) { this.description_ = this.textBox_.value; this.resetDescriptionText_(); } this.cleanUpEdit_(); if (changed) { this.save(); } } // Resets the task description text based on the current value of // this.description_. Task.prototype.resetDescriptionText_ = function() { this.descriptionContainer_.innerHTML = ' '; var descriptionText = this.createElement_("span", null, "text"); descriptionText.appendChild(document.createTextNode(this.description_)); this.descriptionContainer_.insertBefore( descriptionText, this.descriptionContainer_.firstChild); } // Saves this task to the datastore on the server with an AJAX request. Task.prototype.save = function() { var args = [ "list=" + encodeURIComponent(this.listKey_), "description=" + encodeURIComponent(this.description_) ]; if (this.key_) { args.push("task=" + encodeURIComponent(this.key_)); } download("/edittask.do", callback(this, this.onSave_), { post: true, body: args.join("&") }); } // Called when the save task AJAX request finishes. Task.prototype.onSave_ = function(text, status) { if (status >= 200 && status < 300) { this.key_ = text; } } // Let the user drag this task up and down. We remove our element from the // DOM and replace it with a placeholder so the user can see where the // task will snap into place when it is dropped. Task.prototype.onDragStart_ = function() { var placeholder = document.createElement("div"); placeholder.style.width = this.element_.offsetWidth + "px"; placeholder.style.height = this.element_.offsetHeight + "px"; this.element_.parentNode.insertBefore(placeholder, this.element_); setOpacity(this.element_, 0.5); this.placeholder_ = placeholder; } // Reposition our placeholder based on the current position of the task // being dragged. Task.prototype.onDrag_ = function() { var container = this.element_.parentNode; var top = this.element_.offsetTop; var bottom = this.element_.offsetTop + this.element_.offsetHeight; for (var sibling = container.firstChild; sibling != null; sibling = sibling.nextSibling) { if (sibling == this.element_ || sibling == this.placeholder_) continue; var siblingTop = sibling.offsetTop; var siblingBottom = sibling.offsetTop + sibling.offsetHeight; var siblingMiddle = (siblingTop + siblingBottom) / 2; if (siblingTop > bottom) continue; if (siblingBottom < top) continue; if (siblingTop < top && top < siblingMiddle) { if (this.placeholder_.nextChild != sibling) { container.removeChild(this.placeholder_); container.insertBefore(this.placeholder_, sibling); return; } } if (bottom > siblingMiddle) { if (sibling.nextChild != this.placeholder_) { container.removeChild(this.placeholder_); container.insertBefore(this.placeholder_, sibling.nextSibling); return; } } } } // Place our task back into the task list DOM and get rid of the placeholder. Task.prototype.onDragEnd_ = function() { if (!this.placeholder_) return; var container = this.element_.parentNode; container.removeChild(this.element_); this.element_.style.position = "relative"; this.element_.style.width = "auto"; this.element_.style.height = "auto"; this.element_.style.left = "auto"; this.element_.style.top = "auto"; setOpacity(this.element_, 1); container.insertBefore(this.element_, this.placeholder_); container.removeChild(this.placeholder_); this.placeholder_ = null; Event.trigger(this, "positionchanged"); } // Creates a DOM element with the given name, parent, and class name. Task.prototype.createElement_ = function(name, opt_parent, opt_className) { var element = document.createElement(name); if (opt_className) { element.className = opt_className; } if (opt_parent) { opt_parent.appendChild(element); } return element; } // A task list, which is just a collection of tasks. function TaskList(key, tasks) { this.key_ = key; this.tasks_ = tasks; } // Draws this task list in the given container. TaskList.prototype.attach = function(container) { var element = document.createElement("div"); element.className = "tasklist"; element.style.position = "relative"; container.appendChild(element); this.element_ = element; var order = []; for (var i = 0; i < this.tasks_.length; i++) { var task = this.tasks_[i]; order.push(task.key()); var taskElement = task.attach(element); taskElement.task = task; Event.addListener(task, "positionchanged", callback(this, this.savePositions_)); } this.order_ = order; } // Serializes the order of all of the tasks in this list to the server so // the order will be preserved on refresh. TaskList.prototype.savePositions_ = function() { // Determine the task order based on the positions of the DIVs var order = []; for (var child = this.element_.firstChild; child != null; child = child.nextSibling) { if (child.task) { order.push(child.task.key()); } } // Only save the order to the server if it has changed var changed = false; for (var i = 0; i < order.length; i++) { if (order[i] != this.order_[i]) { changed = true; break; } } if (!changed) return; this.order_ = order; // Save the order to the server var body = "tasks=" + encodeURIComponent(order.join(",")); download("/settaskpositions.do", null, { post: true, body: body }); } // Creates a new task in this list TaskList.prototype.newTask = function() { var task = new Task(this.key_, null, "", false); this.tasks_.push(task); var taskElement = task.attach(this.element_); taskElement.task = task; Event.addListener(task, "positionchanged", callback(this, this.savePositions_)); task.edit(); } // Export our API symbols function exportSymbol(name, symbol) { window[name] = symbol; } exportSymbol("Task", Task); exportSymbol("TaskList", TaskList); exportSymbol("DialogBox", DialogBox); exportSymbol("download", download);