1018 lines
32 KiB
JavaScript
1018 lines
32 KiB
JavaScript
|
var create_drag_box = function(div)
|
||
|
{
|
||
|
var create_handle = function(cursor, style)
|
||
|
{
|
||
|
var handle = $(document.createElement("div"));
|
||
|
handle.style.position = "absolute";
|
||
|
handle.className = "frame-box-handle " + cursor;
|
||
|
handle.frame_drag_cursor = cursor;
|
||
|
|
||
|
handle.style.pointerEvents = "all";
|
||
|
div.appendChild(handle);
|
||
|
for(s in style)
|
||
|
{
|
||
|
handle.style[s] = style[s];
|
||
|
}
|
||
|
return handle;
|
||
|
}
|
||
|
|
||
|
/* Create the corner handles after the edge handles, so they're on top. */
|
||
|
create_handle("n-resize", {top: "-5px", width: "100%", height: "10px"});
|
||
|
create_handle("s-resize", {bottom: "-5px", width: "100%", height: "10px"});
|
||
|
create_handle("w-resize", {left: "-5px", height: "100%", width: "10px"});
|
||
|
create_handle("e-resize", {right: "-5px", height: "100%", width: "10px"});
|
||
|
create_handle("nw-resize", {top: "-5px", left: "-5px", height: "10px", width: "10px"});
|
||
|
create_handle("ne-resize", {top: "-5px", right: "-5px", height: "10px", width: "10px"});
|
||
|
create_handle("sw-resize", {bottom: "-5px", left: "-5px", height: "10px", width: "10px"});
|
||
|
create_handle("se-resize", {bottom: "-5px", right: "-5px", height: "10px", width: "10px"});
|
||
|
}
|
||
|
|
||
|
var apply_drag = function(dragging_mode, x, y, image_dimensions, box)
|
||
|
{
|
||
|
var move_modes = {
|
||
|
"move": { left: +1, top: +1, bottom: +1, right: +1 },
|
||
|
"n-resize": { top: +1 },
|
||
|
"s-resize": { bottom: +1 },
|
||
|
"w-resize": { left: +1 },
|
||
|
"e-resize": { right: +1 },
|
||
|
"nw-resize": { top: +1, left: +1 },
|
||
|
"ne-resize": { top: +1, right: +1 },
|
||
|
"sw-resize": { bottom: +1, left: +1 },
|
||
|
"se-resize": { bottom: +1, right: +1 }
|
||
|
}
|
||
|
var mode = move_modes[dragging_mode];
|
||
|
var result = {
|
||
|
left: box.left,
|
||
|
top: box.top,
|
||
|
width: box.width,
|
||
|
height: box.height
|
||
|
};
|
||
|
var right = result.left + result.width;
|
||
|
var bottom = result.top + result.height;
|
||
|
|
||
|
if(dragging_mode == "move")
|
||
|
{
|
||
|
/* In move mode, clamp the movement. In other modes, clip the size below. */
|
||
|
x = clamp(x, -result.left, image_dimensions.width-right);
|
||
|
y = clamp(y, -result.top, image_dimensions.height-bottom);
|
||
|
}
|
||
|
|
||
|
/* Apply the drag. */
|
||
|
if(mode.top != null) result.top += y * mode.top;
|
||
|
if(mode.left != null) result.left += x * mode.left;
|
||
|
if(mode.right != null) right += x * mode.right;
|
||
|
if(mode.bottom != null) bottom += y * mode.bottom;
|
||
|
|
||
|
if(dragging_mode != "move")
|
||
|
{
|
||
|
/* Only clamp the dimensions that were modified. */
|
||
|
if(mode.left != null) result.left = clamp(result.left, 0, right-1);
|
||
|
if(mode.top != null) result.top = clamp(result.top, 0, bottom-1);
|
||
|
if(mode.bottom != null) bottom = clamp(bottom, result.top+1, image_dimensions.height);
|
||
|
if(mode.right != null) right = clamp(right, result.left+1, image_dimensions.width);
|
||
|
}
|
||
|
|
||
|
result.width = right - result.left;
|
||
|
result.height = bottom - result.top;
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Given a frame, its post and an image, return the frame's rectangle scaled to
|
||
|
* the size of the image.
|
||
|
*/
|
||
|
var frame_dimensions_to_image = function(frame, image, post)
|
||
|
{
|
||
|
var result = {
|
||
|
top: frame.source_top,
|
||
|
left: frame.source_left,
|
||
|
width: frame.source_width,
|
||
|
height: frame.source_height
|
||
|
};
|
||
|
result.left *= image.width / post.width;
|
||
|
result.top *= image.height / post.height;
|
||
|
result.width *= image.width / post.width;
|
||
|
result.height *= image.height / post.height;
|
||
|
|
||
|
result.top = Math.round(result.top); result.left = Math.round(result.left);
|
||
|
result.width = Math.round(result.width); result.height = Math.round(result.height);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Convert dimensions scaled to an image back to the source resolution.
|
||
|
*/
|
||
|
var frame_dimensions_from_image = function(frame, image, post)
|
||
|
{
|
||
|
var result = {
|
||
|
source_top: frame.top,
|
||
|
source_left: frame.left,
|
||
|
source_width: frame.width,
|
||
|
source_height: frame.height
|
||
|
};
|
||
|
|
||
|
/* Scale the coordinates back into the source resolution. */
|
||
|
result.source_top /= image.height / post.height;
|
||
|
result.source_left /= image.width / post.width;
|
||
|
result.source_height /= image.height / post.height;
|
||
|
result.source_width /= image.width / post.width;
|
||
|
|
||
|
result.source_top = Math.round(result.source_top); result.source_left = Math.round(result.source_left);
|
||
|
result.source_width = Math.round(result.source_width); result.source_height = Math.round(result.source_height);
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
FrameEditor = function(container, image_container, popup_container, options)
|
||
|
{
|
||
|
this.container = container;
|
||
|
this.popup_container = popup_container;
|
||
|
this.image_container = image_container;
|
||
|
this.options = options;
|
||
|
this.show_corner_drag = true;
|
||
|
|
||
|
this.image_frames = [];
|
||
|
|
||
|
/* Event handlers which are set only while the tag editor is open: */
|
||
|
this.open_handlers = [];
|
||
|
|
||
|
/* Set up the four parts of the corner dragger. */
|
||
|
var popup_parts = [".frame-editor-nw", ".frame-editor-ne", ".frame-editor-sw", ".frame-editor-se"];
|
||
|
this.corner_draggers = [];
|
||
|
for(var i = 0; i < popup_parts.length; ++i)
|
||
|
{
|
||
|
var part = popup_parts[i];
|
||
|
var div = this.popup_container.down(part);
|
||
|
var corner_dragger = new CornerDragger(div, part, {
|
||
|
onUpdate: function() {
|
||
|
this.update_frame_in_list(this.editing_frame);
|
||
|
this.update_image_frame(this.editing_frame);
|
||
|
}.bind(this)
|
||
|
});
|
||
|
this.corner_draggers.push(corner_dragger);
|
||
|
}
|
||
|
|
||
|
/* Create the main frame. This sits on top of the image, receives mouse events and
|
||
|
* holds the individual frames. */
|
||
|
var div = $(document.createElement("div"));
|
||
|
div.style.position = "absolute";
|
||
|
div.style.left = "0";
|
||
|
div.style.top = "0";
|
||
|
div.className = "frame-editor-main-frame";
|
||
|
this.image_container.appendChild(div);
|
||
|
this.main_frame = div;
|
||
|
this.main_frame.hide();
|
||
|
|
||
|
/* Frame editor buttons: */
|
||
|
this.container.down(".frame-editor-add").on("click", function(e) { e.stop(); this.add_frame(); }.bindAsEventListener(this));
|
||
|
|
||
|
/* Buttons in the frame table: */
|
||
|
this.container.on("click", ".frame-label", function(e, element) {
|
||
|
e.stop();
|
||
|
var frame_idx = element.up(".frame-row").frame_idx;
|
||
|
this.focus(frame_idx);
|
||
|
}.bind(this));
|
||
|
|
||
|
this.container.on("click", ".frame-delete", function(e, element) {
|
||
|
e.stop();
|
||
|
var frame_idx = element.up(".frame-row").frame_idx;
|
||
|
this.delete_frame(frame_idx);
|
||
|
}.bind(this));
|
||
|
|
||
|
this.container.on("click", ".frame-up", function(e, element) {
|
||
|
e.stop();
|
||
|
var frame_idx = element.up(".frame-row").frame_idx;
|
||
|
this.move_frame(frame_idx, frame_idx-1);
|
||
|
}.bind(this));
|
||
|
|
||
|
this.container.on("click", ".frame-down", function(e, element) {
|
||
|
e.stop();
|
||
|
var frame_idx = element.up(".frame-row").frame_idx;
|
||
|
this.move_frame(frame_idx, frame_idx+1);
|
||
|
}.bind(this));
|
||
|
|
||
|
this.container.down("table").on("change", function(e) {
|
||
|
this.form_data_changed();
|
||
|
}.bind(this));
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.move_frame = function(frame_idx, frame_idx_target)
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
|
||
|
frame_idx_target = Math.max(frame_idx_target, 0);
|
||
|
frame_idx_target = Math.min(frame_idx_target, post.frames_pending.length-1);
|
||
|
if(frame_idx == frame_idx_target)
|
||
|
return;
|
||
|
|
||
|
var frame = post.frames_pending[frame_idx];
|
||
|
post.frames_pending.splice(frame_idx, 1);
|
||
|
post.frames_pending.splice(frame_idx_target, 0, frame);
|
||
|
|
||
|
this.repopulate_table();
|
||
|
|
||
|
/* Reset the focus. If the item that was moved was focused, focus on it in
|
||
|
* its new position. */
|
||
|
var editing_frame = this.editing_frame == frame_idx? frame_idx_target:this.editing_frame;
|
||
|
this.editing_frame = null;
|
||
|
this.focus(editing_frame);
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.form_data_changed = function()
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
for(var i = 0; i < post.frames_pending.length; ++i)
|
||
|
this.update_frame_from_list(i);
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.set_drag_to_create = function(enable)
|
||
|
{
|
||
|
this.drag_to_create = enable;
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.update_show_corner_drag = function()
|
||
|
{
|
||
|
var shown = this.post_id != null && this.editing_frame != null && this.show_corner_drag;
|
||
|
if(Prototype.Browser.WebKit)
|
||
|
{
|
||
|
/* Work around a WebKit (maybe just a Chrome) issue. Images are downloaded immediately, but
|
||
|
* they're only decompressed the first time they're actually painted on screen. This happens
|
||
|
* late, after all style is applied: hiding with display: none, visibility: hidden or even
|
||
|
* opacity: 0 causes the image to not be decoded until it's displayed, which causes a huge
|
||
|
* UI hitch the first time the user drags a box. Work around this by setting opacity very
|
||
|
* small; it'll trick it into decoding the image, but it'll clip to 0 when rendered. */
|
||
|
if(shown)
|
||
|
{
|
||
|
this.popup_container.style.opacity = 1;
|
||
|
this.popup_container.style.pointerEvents = "";
|
||
|
this.popup_container.style.position = "static";
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
this.popup_container.style.opacity = 0.001;
|
||
|
|
||
|
/* Make sure the invisible element doesn't interfere with the page; disable pointer-events
|
||
|
* so it doesn't receive clicks, and set it to absolute so it doesn't affect the size of its
|
||
|
* containing box. */
|
||
|
this.popup_container.style.pointerEvents = "none";
|
||
|
this.popup_container.style.position = "absolute";
|
||
|
this.popup_container.style.top = "0px";
|
||
|
this.popup_container.style.right = "0px";
|
||
|
}
|
||
|
this.popup_container.show();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
this.popup_container.show(shown);
|
||
|
}
|
||
|
|
||
|
for(var i = 0; i < this.corner_draggers.length; ++i)
|
||
|
this.corner_draggers[i].update();
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.set_show_corner_drag = function(enable)
|
||
|
{
|
||
|
this.show_corner_drag = enable;
|
||
|
this.update_show_corner_drag();
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.set_image_dimensions = function(width, height)
|
||
|
{
|
||
|
var editing_frame = this.editing_frame;
|
||
|
var post_id = this.post_id;
|
||
|
|
||
|
this.close();
|
||
|
|
||
|
this.image_dimensions = {width: width, height: height};
|
||
|
this.main_frame.style.width = this.image_dimensions.width + "px";
|
||
|
this.main_frame.style.height = this.image_dimensions.height + "px";
|
||
|
|
||
|
if(post_id != null)
|
||
|
{
|
||
|
this.open(post_id);
|
||
|
this.focus(editing_frame);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Like document.elementFromPoint, but returns an array of all elements at the given point.
|
||
|
* If a top element is specified, stop if it's reached without including it in the list.
|
||
|
*
|
||
|
*/
|
||
|
var elementArrayFromPoint = function(x, y, top)
|
||
|
{
|
||
|
var elements = [];
|
||
|
while(1)
|
||
|
{
|
||
|
var element = document.elementFromPoint(x, y);
|
||
|
if(element == this.main_frame || element == document.documentElement)
|
||
|
break;
|
||
|
element.original_display = element.style.display;
|
||
|
element.style.display = "none";
|
||
|
elements.push(element);
|
||
|
}
|
||
|
|
||
|
/* Restore the elements we just hid. */
|
||
|
elements.each(function(e) {
|
||
|
e.style.display = e.original_display;
|
||
|
e.original_display = null;
|
||
|
});
|
||
|
|
||
|
return elements;
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.is_opened = function()
|
||
|
{
|
||
|
return this.post_id != null;
|
||
|
}
|
||
|
|
||
|
/* Open the frame editor if it isn't already, and focus on the specified frame. */
|
||
|
FrameEditor.prototype.open = function(post_id)
|
||
|
{
|
||
|
if(this.image_dimensions == null)
|
||
|
throw "Must call set_image_dimensions before open";
|
||
|
if(this.post_id != null)
|
||
|
return;
|
||
|
this.post_id = post_id;
|
||
|
this.editing_frame = null;
|
||
|
this.dragging_item = null;
|
||
|
|
||
|
this.container.show();
|
||
|
this.main_frame.show();
|
||
|
this.update_show_corner_drag();
|
||
|
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
|
||
|
/* Tell the corner draggers which post we're working on now, so they'll start
|
||
|
* loading the JPEG version immediately if necessary. Otherwise, we'll start
|
||
|
* loading it the first time we focus a frame, which will hitch the editor for
|
||
|
* a while in Chrome. */
|
||
|
for(var i = 0; i < this.corner_draggers.length; ++i)
|
||
|
this.corner_draggers[i].set_post_id(this.post_id);
|
||
|
|
||
|
this.open_handlers.push(
|
||
|
document.on("keydown", function(e) {
|
||
|
if (e.keyCode == Event.KEY_ESC) { this.discard(); }
|
||
|
}.bindAsEventListener(this))
|
||
|
)
|
||
|
|
||
|
/* If we havn't done so already, make a backup of this post's frames. We'll restore
|
||
|
* from this later if the user cancels the edit. */
|
||
|
this.original_frames = Object.toJSON(post.frames_pending);
|
||
|
|
||
|
this.repopulate_table();
|
||
|
|
||
|
this.create_dragger();
|
||
|
|
||
|
if(post.frames_pending.length > 0)
|
||
|
this.focus(0);
|
||
|
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.create_dragger = function()
|
||
|
{
|
||
|
if(this.dragger)
|
||
|
this.dragger.destroy();
|
||
|
|
||
|
this.dragger = new DragElement(this.main_frame, {
|
||
|
ondown: function(e) {
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
|
||
|
/*
|
||
|
* Figure out which element(s) we're clicking on. The click may lie on a spot
|
||
|
* where multiple frames overlap; make a list.
|
||
|
*
|
||
|
* Temporarily enable pointerEvents on the frames, so elementFromPoint will
|
||
|
* resolve them.
|
||
|
*/
|
||
|
this.image_frames.each(function(frame) { frame.style.pointerEvents = "all"; });
|
||
|
var clicked_elements = elementArrayFromPoint(e.x, e.y, this.main_frame);
|
||
|
this.image_frames.each(function(frame) { frame.style.pointerEvents = "none"; });
|
||
|
|
||
|
/* If we clicked on a handle, prefer it over frame bodies at the same spot. */
|
||
|
var element = null;
|
||
|
clicked_elements.each(function(e) {
|
||
|
/* If a handle was clicked, always prefer it. Use the first handle we find,
|
||
|
* so we prefer the corner handles (which are always on top) to edge handles. */
|
||
|
if(element == null && e.hasClassName("frame-box-handle"))
|
||
|
element = e;
|
||
|
}.bind(this));
|
||
|
|
||
|
/* If a handle wasn't clicked, prefer the frame that's currently focused. */
|
||
|
if(element == null)
|
||
|
{
|
||
|
clicked_elements.each(function(e) {
|
||
|
if(!e.hasClassName("frame-editor-frame-box"))
|
||
|
e = e.up(".frame-editor-frame-box");
|
||
|
if(this.image_frames.indexOf(e) == this.editing_frame)
|
||
|
element = e;
|
||
|
}.bind(this));
|
||
|
}
|
||
|
|
||
|
/* Otherwise, just use the first item that was found. */
|
||
|
if(element == null)
|
||
|
element = clicked_elements[0];
|
||
|
|
||
|
/* If a handle was clicked on, find the frame element that contains it. */
|
||
|
var frame_element = element;
|
||
|
if(!frame_element.hasClassName("frame-editor-frame-box"))
|
||
|
frame_element = frame_element.up(".frame-editor-frame-box");
|
||
|
|
||
|
/* If we didn't click on a frame box at all, create a new one. */
|
||
|
if(frame_element == null)
|
||
|
{
|
||
|
if(!this.drag_to_create)
|
||
|
return;
|
||
|
|
||
|
this.dragging_new = true;
|
||
|
}
|
||
|
else
|
||
|
this.dragging_new = false;
|
||
|
|
||
|
/* If the element we actually clicked on was one of the edge handles, set the drag
|
||
|
* mode based on which one was clicked. */
|
||
|
if(element.hasClassName("frame-box-handle"))
|
||
|
this.dragging_mode = element.frame_drag_cursor
|
||
|
else
|
||
|
this.dragging_mode = "move";
|
||
|
|
||
|
if(frame_element && frame_element.hasClassName("frame-editor-frame-box"))
|
||
|
{
|
||
|
var frame_idx = this.image_frames.indexOf(frame_element);
|
||
|
this.dragging_idx = frame_idx;
|
||
|
|
||
|
var frame = post.frames_pending[this.dragging_idx];
|
||
|
this.dragging_anchor = frame_dimensions_to_image(frame, this.image_dimensions, post);
|
||
|
}
|
||
|
|
||
|
this.focus(this.dragging_idx);
|
||
|
|
||
|
/* If we're dragging a handle, override the drag class so the pointer will
|
||
|
* use the handle pointer instead of the drag pointer. */
|
||
|
this.dragger.overriden_drag_class = this.dragging_mode == "move"? null: this.dragging_mode;
|
||
|
|
||
|
this.dragger.options.snap_pixels = this.dragging_new? 10:0;
|
||
|
|
||
|
/* Stop propagation of the event, so any other draggers in the chain don't start. In
|
||
|
* particular, when we're dragging inside the image, we need to stop WindowDragElementAbsolute.
|
||
|
* Only do this if we're actually dragging, not if we aborted due to this.drag_to_create. */
|
||
|
e.latest_event.stopPropagation();
|
||
|
}.bind(this),
|
||
|
|
||
|
onup: function(e) {
|
||
|
this.dragging_idx = null;
|
||
|
this.dragging_anchor = null;
|
||
|
}.bind(this),
|
||
|
|
||
|
ondrag: function(e) {
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
|
||
|
if(this.dragging_new)
|
||
|
{
|
||
|
/* Pick a dragging mode based on which way we were dragged. This is a
|
||
|
* little funny; we should probably be able to drag freely, not be fixed
|
||
|
* to the first direction we drag. */
|
||
|
if(e.aX > 0 && e.aY > 0) this.dragging_mode = "se-resize";
|
||
|
else if(e.aX > 0 && e.aY < 0) this.dragging_mode = "ne-resize";
|
||
|
else if(e.aX < 0 && e.aY > 0) this.dragging_mode = "sw-resize";
|
||
|
else if(e.aX < 0 && e.aY < 0) this.dragging_mode = "nw-resize";
|
||
|
else return;
|
||
|
|
||
|
this.dragging_new = false;
|
||
|
|
||
|
/* Create a new, empty frame. When we get to the regular drag path below we'll
|
||
|
* give it its real size, based on how far we've dragged so far. */
|
||
|
var frame_offset = this.main_frame.cumulativeOffset();
|
||
|
var dims = {
|
||
|
left: e.dragger.anchor_x - frame_offset.left,
|
||
|
top: e.dragger.anchor_y - frame_offset.top,
|
||
|
height: 0,
|
||
|
width: 0
|
||
|
};
|
||
|
this.dragging_anchor = dims;
|
||
|
|
||
|
var source_dims = frame_dimensions_from_image(dims, this.image_dimensions, post);
|
||
|
this.dragging_idx = this.add_frame(source_dims);
|
||
|
post.frames_pending[this.editing_frame] = source_dims;
|
||
|
}
|
||
|
|
||
|
if(this.dragging_idx == null)
|
||
|
return;
|
||
|
|
||
|
var dims = apply_drag(this.dragging_mode, e.aX, e.aY, this.image_dimensions, this.dragging_anchor);
|
||
|
|
||
|
/* Scale the changed dimensions back to the source resolution and apply them
|
||
|
* to the frame. */
|
||
|
var source_dims = frame_dimensions_from_image(dims, this.image_dimensions, post);
|
||
|
post.frames_pending[this.editing_frame] = source_dims;
|
||
|
|
||
|
this.update_frame_in_list(this.editing_frame);
|
||
|
this.update_image_frame(this.editing_frame);
|
||
|
}.bind(this)
|
||
|
});
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.repopulate_table = function()
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
|
||
|
/* Clear the table. */
|
||
|
var tbody = this.container.down(".frame-list").down("TBODY");
|
||
|
while(tbody.firstChild)
|
||
|
tbody.removeChild(tbody.firstChild);
|
||
|
|
||
|
/* Clear the image frames. */
|
||
|
this.image_frames.each(function(f) {
|
||
|
f.parentNode.removeChild(f);
|
||
|
}.bind(this));
|
||
|
this.image_frames = [];
|
||
|
|
||
|
for(var i = 0; i < post.frames_pending.length; ++i)
|
||
|
{
|
||
|
this.add_frame_to_list(i);
|
||
|
this.create_image_frame();
|
||
|
this.update_image_frame(i);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.update = function()
|
||
|
{
|
||
|
this.update_show_corner_drag();
|
||
|
|
||
|
if(this.image_dimensions == null)
|
||
|
return;
|
||
|
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
if(post != null)
|
||
|
{
|
||
|
for(var i = 0; i < post.frames_pending.length; ++i)
|
||
|
this.update_image_frame(i);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* If the frame editor is open, discard changes and close it. */
|
||
|
FrameEditor.prototype.discard = function()
|
||
|
{
|
||
|
if(this.post_id == null)
|
||
|
return;
|
||
|
|
||
|
/* Save revert_to, and close the editor before reverting, to make sure closing
|
||
|
* the editor doesn't change anything. */
|
||
|
var revert_to = this.original_frames;
|
||
|
var post_id = this.post_id;
|
||
|
this.close();
|
||
|
|
||
|
/* Revert changes. */
|
||
|
var post = Post.posts.get(post_id);
|
||
|
post.frames_pending = revert_to.evalJSON();
|
||
|
}
|
||
|
|
||
|
/* Get the frames specifier for the post's frames. */
|
||
|
FrameEditor.prototype.get_current_frames_spec = function()
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
var frame = post.frames_pending;
|
||
|
var frame_specs = [];
|
||
|
post.frames_pending.each(function(frame) {
|
||
|
var s = frame.source_left + "x" + frame.source_top + "," + frame.source_width + "x" + frame.source_height;
|
||
|
frame_specs.push(s);
|
||
|
}.bind(this));
|
||
|
return frame_specs.join(";");
|
||
|
}
|
||
|
|
||
|
|
||
|
/* Return true if the frames have been changed. */
|
||
|
FrameEditor.prototype.changed = function()
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
var spec = this.get_current_frames_spec();
|
||
|
return spec != post.frames_pending_string;
|
||
|
}
|
||
|
|
||
|
/* Save changes to the post, if any. If not null, call finished on completion. */
|
||
|
FrameEditor.prototype.save = function(finished)
|
||
|
{
|
||
|
if(this.post_id == null)
|
||
|
{
|
||
|
if(finished)
|
||
|
finished();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/* Save the current post_id, so it's preserved when the AJAX completion function
|
||
|
* below is run. */
|
||
|
var post_id = this.post_id;
|
||
|
var post = Post.posts.get(post_id);
|
||
|
var frame = post.frames_pending;
|
||
|
|
||
|
var spec = this.get_current_frames_spec();
|
||
|
if(spec == post.frames_pending_string)
|
||
|
{
|
||
|
if(finished)
|
||
|
finished();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Post.update_batch([{
|
||
|
id: post_id,
|
||
|
frames_pending_string: spec
|
||
|
}], function(posts)
|
||
|
{
|
||
|
if(this.post_id == post_id)
|
||
|
{
|
||
|
/* The registered post has been changed, and we're still displaying it. Grab the
|
||
|
* new version, and updated original_frames so we no longer consider this post
|
||
|
* changed. */
|
||
|
var post = Post.posts.get(post_id);
|
||
|
this.original_frames = Object.toJSON(post.frames_pending);
|
||
|
|
||
|
/* In the off-chance that the frames_pending that came back differs from what we
|
||
|
* requested, update the display. */
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
if(finished)
|
||
|
finished();
|
||
|
}.bind(this));
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.create_image_frame = function()
|
||
|
{
|
||
|
var div = $(document.createElement("div"));
|
||
|
div.className = "frame-editor-frame-box";
|
||
|
|
||
|
/* Disable pointer-events on the image frame, so the handle cursors always
|
||
|
* show up even when an image frame lies on top of it. */
|
||
|
div.style.pointerEvents = "none";
|
||
|
|
||
|
// div.style.opacity=0.1;
|
||
|
this.main_frame.appendChild(div);
|
||
|
this.image_frames.push(div);
|
||
|
|
||
|
create_drag_box(div);
|
||
|
|
||
|
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.update_image_frame = function(frame_idx)
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
var frame = post.frames_pending[frame_idx];
|
||
|
|
||
|
/* If the focused frame is being modified, update the corner dragger as well. */
|
||
|
if(frame_idx == this.editing_frame)
|
||
|
{
|
||
|
for(var i = 0; i < this.corner_draggers.length; ++i)
|
||
|
this.corner_draggers[i].update();
|
||
|
}
|
||
|
|
||
|
var dimensions = frame_dimensions_to_image(frame, this.image_dimensions, post);
|
||
|
|
||
|
var div = this.image_frames[frame_idx];
|
||
|
div.style.left = dimensions.left + "px";
|
||
|
div.style.top = dimensions.top + "px";
|
||
|
div.style.width = dimensions.width + "px";
|
||
|
div.style.height = dimensions.height + "px";
|
||
|
|
||
|
if(frame_idx == this.editing_frame)
|
||
|
div.addClassName("focused-frame-box");
|
||
|
else
|
||
|
div.removeClassName("focused-frame-box");
|
||
|
}
|
||
|
|
||
|
/* Append the given frame to the editor list. */
|
||
|
FrameEditor.prototype.add_frame_to_list = function(frame_idx)
|
||
|
{
|
||
|
var tbody = this.container.down(".frame-list").down("TBODY");
|
||
|
var tr = $(document.createElement("TR"));
|
||
|
tr.className = "frame-row frame-" + frame_idx;
|
||
|
tr.frame_idx = frame_idx;
|
||
|
tbody.appendChild(tr);
|
||
|
|
||
|
var html = "<td><span class='frame-label'>Frame " + frame_idx + "</span></td>";
|
||
|
html += "<td><input class='frame-left frame-dims' size=4></td>";
|
||
|
html += "<td><input class='frame-top frame-dims' size=4></td>";
|
||
|
html += "<td><input class='frame-width frame-dims' size=4></td>";
|
||
|
html += "<td><input class='frame-height frame-dims' size=4></td>";
|
||
|
html += "<td><a class='frame-delete frame-button-box' href='#'>X</a></td>";
|
||
|
html += "<td><a class='frame-up frame-button-box' href='#'>⇡</a></td>";
|
||
|
html += "<td><a class='frame-down frame-button-box' href='#'>⇣</a></td>";
|
||
|
tr.innerHTML = html;
|
||
|
|
||
|
this.update_frame_in_list(frame_idx);
|
||
|
}
|
||
|
|
||
|
/* Update the fields of frame_idx in the table. */
|
||
|
FrameEditor.prototype.update_frame_in_list = function(frame_idx)
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
var frame = post.frames_pending[frame_idx];
|
||
|
|
||
|
var tbody = this.container.down(".frame-list").down("TBODY");
|
||
|
var tr = tbody.down(".frame-" + frame_idx);
|
||
|
|
||
|
tr.down(".frame-left").value = frame.source_left;
|
||
|
tr.down(".frame-top").value = frame.source_top;
|
||
|
tr.down(".frame-width").value = frame.source_width;
|
||
|
tr.down(".frame-height").value = frame.source_height;
|
||
|
}
|
||
|
|
||
|
/* Commit changes in the frame list to the frame. */
|
||
|
FrameEditor.prototype.update_frame_from_list = function(frame_idx)
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
var frame = post.frames_pending[frame_idx];
|
||
|
|
||
|
var tbody = this.container.down(".frame-list").down("TBODY");
|
||
|
var tr = tbody.down(".frame-" + frame_idx);
|
||
|
|
||
|
frame.source_left = tr.down(".frame-left").value;
|
||
|
frame.source_top = tr.down(".frame-top").value;
|
||
|
frame.source_width = tr.down(".frame-width").value;
|
||
|
frame.source_height = tr.down(".frame-height").value;
|
||
|
}
|
||
|
|
||
|
/* Add a new default frame to the end of the list, update the table, and edit the new frame. */
|
||
|
FrameEditor.prototype.add_frame = function(new_frame)
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
|
||
|
if(new_frame == null)
|
||
|
new_frame = {
|
||
|
source_top: post.height * 1/4,
|
||
|
source_left: post.width * 1/4,
|
||
|
source_width: post.width / 2,
|
||
|
source_height: post.height / 2
|
||
|
};
|
||
|
|
||
|
post.frames_pending.push(new_frame);
|
||
|
this.add_frame_to_list(post.frames_pending.length-1);
|
||
|
this.create_image_frame();
|
||
|
this.update_image_frame(post.frames_pending.length-1);
|
||
|
|
||
|
this.focus(post.frames_pending.length-1);
|
||
|
return post.frames_pending.length-1;
|
||
|
}
|
||
|
|
||
|
/* Delete the specified frame. */
|
||
|
FrameEditor.prototype.delete_frame = function(frame_idx)
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
|
||
|
/* If we're editing this frame, switch to a nearby one. */
|
||
|
var switch_to_frame = null;
|
||
|
if(this.editing_frame == frame_idx)
|
||
|
{
|
||
|
switch_to_frame = this.editing_frame;
|
||
|
this.focus(null);
|
||
|
|
||
|
/* If we're deleting the bottom item on the list, switch to the item above it instead. */
|
||
|
if(frame_idx == post.frames_pending.length-1)
|
||
|
--switch_to_frame;
|
||
|
|
||
|
/* If that put it over the top, we're deleting the only item. Focus no item. */
|
||
|
if(switch_to_frame < 0)
|
||
|
switch_to_frame = null;
|
||
|
}
|
||
|
|
||
|
/* Remove the frame from the array. */
|
||
|
post.frames_pending.splice(frame_idx, 1);
|
||
|
|
||
|
/* Renumber the table. */
|
||
|
this.repopulate_table();
|
||
|
|
||
|
/* Focus switch_to_frame, if any. */
|
||
|
this.focus(switch_to_frame);
|
||
|
}
|
||
|
|
||
|
FrameEditor.prototype.focus = function(post_frame)
|
||
|
{
|
||
|
if(this.editing_frame == post_frame)
|
||
|
return;
|
||
|
|
||
|
if(this.editing_frame != null)
|
||
|
{
|
||
|
var row = this.container.down(".frame-" + this.editing_frame);
|
||
|
row.removeClassName("frame-focused");
|
||
|
}
|
||
|
|
||
|
this.editing_frame = post_frame;
|
||
|
|
||
|
if(this.editing_frame != null)
|
||
|
{
|
||
|
var row = this.container.down(".frame-" + this.editing_frame);
|
||
|
row.addClassName("frame-focused");
|
||
|
}
|
||
|
|
||
|
for(var i = 0; i < this.corner_draggers.length; ++i)
|
||
|
this.corner_draggers[i].set_post_frame(this.editing_frame);
|
||
|
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
/* Close the frame editor. Local changes are not saved or reverted. */
|
||
|
FrameEditor.prototype.close = function()
|
||
|
{
|
||
|
if(this.post_id == null)
|
||
|
return;
|
||
|
this.post_id = null;
|
||
|
|
||
|
this.editing_frame = null;
|
||
|
|
||
|
for(var i = 0; i < this.corner_draggers.length; ++i)
|
||
|
this.corner_draggers[i].set_post_id(null);
|
||
|
|
||
|
if(this.keydown_handler)
|
||
|
{
|
||
|
this.open_handlers.each(function(h) { h.stop(); });
|
||
|
this.open_handlers = [];
|
||
|
}
|
||
|
|
||
|
if(this.dragger)
|
||
|
this.dragger.destroy();
|
||
|
this.dragger = null;
|
||
|
|
||
|
this.container.hide();
|
||
|
this.main_frame.hide();
|
||
|
this.update_show_corner_drag();
|
||
|
|
||
|
/* Clear the row table. */
|
||
|
var tbody = this.container.down(".frame-list").down("TBODY");
|
||
|
while(tbody.firstChild)
|
||
|
tbody.removeChild(tbody.firstChild);
|
||
|
|
||
|
this.original_frames = null;
|
||
|
this.update();
|
||
|
|
||
|
if(this.options.onClose)
|
||
|
this.options.onClose(this);
|
||
|
}
|
||
|
|
||
|
/* Create the specified corner dragger. */
|
||
|
CornerDragger = function(container, part, options)
|
||
|
{
|
||
|
this.container = container;
|
||
|
this.part = part;
|
||
|
this.options = options;
|
||
|
|
||
|
var box = container.down(".frame-editor-popup-div");
|
||
|
|
||
|
/* Create a div inside each .frame-editor-popup-div floating on top of the image
|
||
|
* to show the border of the frame. */
|
||
|
var frame_box = $(document.createElement("div"));
|
||
|
frame_box.className = "frame-editor-frame-box";
|
||
|
create_drag_box(frame_box);
|
||
|
box.appendChild(frame_box);
|
||
|
|
||
|
this.dragger = new DragElement(box, {
|
||
|
snap_pixels: 0,
|
||
|
|
||
|
ondown: function(e) {
|
||
|
var element = document.elementFromPoint(e.x, e.y);
|
||
|
|
||
|
/* If we clicked on a drag handle, use that handle. Otherwise, choose the corner drag
|
||
|
* handle for the corner we're in. */
|
||
|
if(element.hasClassName("frame-box-handle")) this.dragging_mode = element.frame_drag_cursor;
|
||
|
else if(part == ".frame-editor-nw") this.dragging_mode = "nw-resize";
|
||
|
else if(part == ".frame-editor-ne") this.dragging_mode = "ne-resize";
|
||
|
else if(part == ".frame-editor-sw") this.dragging_mode = "sw-resize";
|
||
|
else if(part == ".frame-editor-se") this.dragging_mode = "se-resize";
|
||
|
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
var frame = post.frames_pending[this.post_frame];
|
||
|
this.dragging_anchor = frame_dimensions_to_image(frame, this.image_dimensions, post);
|
||
|
|
||
|
/* When dragging a handle, hide the cursor to get it out of the way. */
|
||
|
this.dragger.overriden_drag_class = this.dragging_mode == "move"? null: "hide-cursor";
|
||
|
|
||
|
/* Stop propagation of the event, so any other draggers in the chain don't start. In
|
||
|
* particular, when we're dragging inside the image, we need to stop WindowDragElementAbsolute.
|
||
|
* Only do this if we're actually dragging, not if we aborted due to this.drag_to_create. */
|
||
|
e.latest_event.stopPropagation();
|
||
|
}.bind(this),
|
||
|
|
||
|
ondrag: function(e) {
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
|
||
|
/* Invert the motion, since we're dragging the image around underneith the
|
||
|
* crop frame instead of dragging the crop frame around. */
|
||
|
var dims = apply_drag(this.dragging_mode, -e.aX, -e.aY, this.image_dimensions, this.dragging_anchor);
|
||
|
|
||
|
/* Scale the changed dimensions back to the source resolution and apply them
|
||
|
* to the frame. */
|
||
|
var source_dims = frame_dimensions_from_image(dims, this.image_dimensions, post);
|
||
|
post.frames_pending[this.post_frame] = source_dims;
|
||
|
|
||
|
if(this.options.onUpdate)
|
||
|
this.options.onUpdate();
|
||
|
}.bind(this)
|
||
|
});
|
||
|
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* Set the post to show in the corner dragger. If post_id is null, clear any displayed
|
||
|
* post.
|
||
|
*
|
||
|
* When the post ID is set, the post frame is always cleared.
|
||
|
*/
|
||
|
CornerDragger.prototype.set_post_id = function(post_id)
|
||
|
{
|
||
|
this.post_id = post_id;
|
||
|
this.post_frame = null;
|
||
|
|
||
|
var url = null;
|
||
|
var img = this.container.down("img");
|
||
|
if(post_id != null)
|
||
|
{
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
this.image_dimensions = {
|
||
|
width: post.jpeg_width, height: post.jpeg_height
|
||
|
};
|
||
|
|
||
|
url = post.jpeg_url;
|
||
|
img.width = this.image_dimensions.width;
|
||
|
img.height = this.image_dimensions.height;
|
||
|
}
|
||
|
|
||
|
/* Don't change the image if it's already set; it causes Chrome to reprocess the
|
||
|
* image. */
|
||
|
if(img.src != url)
|
||
|
{
|
||
|
img.src = url;
|
||
|
|
||
|
if(Prototype.Browser.WebKit && url)
|
||
|
{
|
||
|
/* Decoding in Chrome takes long enough to be visible. Hourglass the cursor while it runs. */
|
||
|
document.documentElement.addClassName("hourglass");
|
||
|
(function() { document.documentElement.removeClassName("hourglass"); }.defer());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
CornerDragger.prototype.set_post_frame = function(post_frame)
|
||
|
{
|
||
|
this.post_frame = post_frame;
|
||
|
|
||
|
this.update();
|
||
|
}
|
||
|
|
||
|
CornerDragger.prototype.update = function()
|
||
|
{
|
||
|
if(this.post_id == null || this.post_frame == null)
|
||
|
return;
|
||
|
|
||
|
var post = Post.posts.get(this.post_id);
|
||
|
var frame = post.frames_pending[this.post_frame];
|
||
|
var dims = frame_dimensions_to_image(frame, this.image_dimensions, post);
|
||
|
|
||
|
var div = this.container;
|
||
|
|
||
|
/* Update the drag/frame box. */
|
||
|
var box = this.container.down(".frame-editor-frame-box");
|
||
|
box.style.left = dims.left + "px";
|
||
|
box.style.top = dims.top + "px";
|
||
|
box.style.width = dims.width + "px";
|
||
|
box.style.height = dims.height + "px";
|
||
|
|
||
|
/* Recenter the corner box. */
|
||
|
var top = dims.top;
|
||
|
var left = dims.left;
|
||
|
if(this.part == ".frame-editor-ne" || this.part == ".frame-editor-se")
|
||
|
left += dims.width;
|
||
|
if(this.part == ".frame-editor-sw" || this.part == ".frame-editor-se")
|
||
|
top += dims.height;
|
||
|
|
||
|
var offset_height = div.offsetHeight/2;
|
||
|
var offset_width = div.offsetWidth/2;
|
||
|
/*
|
||
|
if(this.part == ".frame-editor-nw" || this.part == ".frame-editor-ne") offset_height -= div.offsetHeight/4;
|
||
|
if(this.part == ".frame-editor-sw" || this.part == ".frame-editor-se") offset_height += div.offsetHeight/4;
|
||
|
if(this.part == ".frame-editor-nw" || this.part == ".frame-editor-sw") offset_width -= div.offsetWidth/4;
|
||
|
if(this.part == ".frame-editor-ne" || this.part == ".frame-editor-se") offset_width += div.offsetWidth/4;
|
||
|
*/
|
||
|
left -= offset_width;
|
||
|
top -= offset_height;
|
||
|
|
||
|
/* If the region is small enough that we don't have enough to fill the corner
|
||
|
* frames, push the frames inward so they line up. */
|
||
|
if(this.part == ".frame-editor-nw" || this.part == ".frame-editor-sw")
|
||
|
left = Math.min(left, dims.left + dims.width/2 - div.offsetWidth);
|
||
|
if(this.part == ".frame-editor-ne" || this.part == ".frame-editor-se")
|
||
|
left = Math.max(left, dims.left + dims.width/2);
|
||
|
if(this.part == ".frame-editor-nw" || this.part == ".frame-editor-ne")
|
||
|
top = Math.min(top, dims.top + dims.height/2 - div.offsetHeight);
|
||
|
if(this.part == ".frame-editor-sw" || this.part == ".frame-editor-se")
|
||
|
top = Math.max(top, dims.top + dims.height/2);
|
||
|
|
||
|
var img = this.container.down(".frame-editor-popup-div");
|
||
|
img.style.marginTop = (-top) + "px";
|
||
|
img.style.marginLeft = (-left) + "px";
|
||
|
}
|
||
|
|