222 lines
7.3 KiB
JavaScript
222 lines
7.3 KiB
JavaScript
|
|
function Handles(options) {
|
|
const { $handles_container, $object_container } = options; // required
|
|
const outset = options.outset || 0;
|
|
const get_handles_offset_left = options.get_handles_offset_left || (() => 0);
|
|
const get_handles_offset_top = options.get_handles_offset_top || (() => 0);
|
|
const get_ghost_offset_left = options.get_ghost_offset_left || (() => 0);
|
|
const get_ghost_offset_top = options.get_ghost_offset_top || (() => 0);
|
|
const size_only = options.size_only || false;
|
|
|
|
const HANDLE_MIDDLE = 0;
|
|
const HANDLE_START = -1;
|
|
const HANDLE_END = 1;
|
|
const HANDLE_LEFT = HANDLE_START;
|
|
const HANDLE_RIGHT = HANDLE_END;
|
|
const HANDLE_TOP = HANDLE_START;
|
|
const HANDLE_BOTTOM = HANDLE_END;
|
|
|
|
const $resize_ghost = $(E("div")).addClass("resize-ghost");
|
|
if (options.thick) {
|
|
$resize_ghost.addClass("thick");
|
|
}
|
|
const handles = [];
|
|
[
|
|
[HANDLE_TOP, HANDLE_RIGHT], // ↗
|
|
[HANDLE_TOP, HANDLE_MIDDLE], // ↑
|
|
[HANDLE_TOP, HANDLE_LEFT], // ↖
|
|
[HANDLE_MIDDLE, HANDLE_LEFT], // ←
|
|
[HANDLE_BOTTOM, HANDLE_LEFT], // ↙
|
|
[HANDLE_BOTTOM, HANDLE_MIDDLE], // ↓
|
|
[HANDLE_BOTTOM, HANDLE_RIGHT], // ↘
|
|
[HANDLE_MIDDLE, HANDLE_RIGHT], // →
|
|
].forEach(([y_axis, x_axis]) => {
|
|
const $h = $(E("div")).addClass("handle");
|
|
$h.appendTo($handles_container);
|
|
const $grab_region = $(E("div")).addClass("grab-region").appendTo($handles_container);
|
|
if (y_axis === HANDLE_MIDDLE || x_axis === HANDLE_MIDDLE) {
|
|
$grab_region.addClass("is-middle");
|
|
}
|
|
|
|
$h.css("touch-action", "none");
|
|
|
|
let rect;
|
|
let dragged = false;
|
|
const resizes_height = y_axis !== HANDLE_MIDDLE;
|
|
const resizes_width = x_axis !== HANDLE_MIDDLE;
|
|
if (size_only && (y_axis === HANDLE_TOP || x_axis === HANDLE_LEFT)) {
|
|
$h.addClass("useless-handle");
|
|
$grab_region.remove();
|
|
} else {
|
|
|
|
let cursor_fname;
|
|
if ((x_axis === HANDLE_LEFT && y_axis === HANDLE_TOP) || (x_axis === HANDLE_RIGHT && y_axis === HANDLE_BOTTOM)) {
|
|
cursor_fname = "nwse-resize";
|
|
} else if ((x_axis === HANDLE_RIGHT && y_axis === HANDLE_TOP) || (x_axis === HANDLE_LEFT && y_axis === HANDLE_BOTTOM)) {
|
|
cursor_fname = "nesw-resize";
|
|
} else if (resizes_width) {
|
|
cursor_fname = "ew-resize";
|
|
} else if (resizes_height) {
|
|
cursor_fname = "ns-resize";
|
|
}
|
|
|
|
let fallback_cursor = "";
|
|
if (y_axis === HANDLE_TOP) { fallback_cursor += "n"; }
|
|
if (y_axis === HANDLE_BOTTOM) { fallback_cursor += "s"; }
|
|
if (x_axis === HANDLE_LEFT) { fallback_cursor += "w"; }
|
|
if (x_axis === HANDLE_RIGHT) { fallback_cursor += "e"; }
|
|
|
|
fallback_cursor += "-resize";
|
|
const cursor = make_css_cursor(cursor_fname, [16, 16], fallback_cursor);
|
|
$h.add($grab_region).css({ cursor });
|
|
|
|
const drag = (event) => {
|
|
$resize_ghost.appendTo($object_container);
|
|
dragged = true;
|
|
|
|
rect = options.get_rect();
|
|
const m = to_canvas_coords(event);
|
|
let delta_x = 0;
|
|
let delta_y = 0;
|
|
let width, height;
|
|
// @TODO: decide between Math.floor/Math.ceil/Math.round for these values
|
|
if (x_axis === HANDLE_RIGHT) {
|
|
delta_x = 0;
|
|
width = ~~(m.x - rect.x);
|
|
} else if (x_axis === HANDLE_LEFT) {
|
|
delta_x = ~~(m.x - rect.x);
|
|
width = ~~(rect.x + rect.width - m.x);
|
|
} else {
|
|
width = ~~(rect.width);
|
|
}
|
|
if (y_axis === HANDLE_BOTTOM) {
|
|
delta_y = 0;
|
|
height = ~~(m.y - rect.y);
|
|
} else if (y_axis === HANDLE_TOP) {
|
|
delta_y = ~~(m.y - rect.y);
|
|
height = ~~(rect.y + rect.height - m.y);
|
|
} else {
|
|
height = ~~(rect.height);
|
|
}
|
|
let new_rect = {
|
|
x: rect.x + delta_x,
|
|
y: rect.y + delta_y,
|
|
width: width,
|
|
height: height,
|
|
};
|
|
|
|
new_rect.width = Math.max(1, new_rect.width);
|
|
new_rect.height = Math.max(1, new_rect.height);
|
|
|
|
if (options.constrain_rect) {
|
|
new_rect = options.constrain_rect(new_rect, x_axis, y_axis);
|
|
} else {
|
|
new_rect.x = Math.min(new_rect.x, rect.x + rect.width);
|
|
new_rect.y = Math.min(new_rect.y, rect.y + rect.height);
|
|
}
|
|
|
|
$resize_ghost.css({
|
|
position: "absolute",
|
|
left: magnification * new_rect.x + get_ghost_offset_left(),
|
|
top: magnification * new_rect.y + get_ghost_offset_top(),
|
|
width: magnification * new_rect.width - 2,
|
|
height: magnification * new_rect.height - 2,
|
|
});
|
|
rect = new_rect;
|
|
};
|
|
$h.add($grab_region).on("pointerdown", event => {
|
|
dragged = false;
|
|
if (event.button === 0) {
|
|
$G.on("pointermove", drag);
|
|
$("body").css({ cursor }).addClass("cursor-bully");
|
|
}
|
|
$G.one("pointerup", () => {
|
|
$G.off("pointermove", drag);
|
|
$("body").css({ cursor: "" }).removeClass("cursor-bully");
|
|
|
|
$resize_ghost.remove();
|
|
if (dragged) {
|
|
options.set_rect(rect);
|
|
}
|
|
$handles_container.trigger("update");
|
|
});
|
|
});
|
|
$h.on("mousedown selectstart", event => {
|
|
event.preventDefault();
|
|
});
|
|
}
|
|
|
|
const update_handle = () => {
|
|
const rect = options.get_rect();
|
|
const hs = $h.width();
|
|
// const x = rect.x + get_handles_offset_left();
|
|
// const y = rect.y + get_handles_offset_top();
|
|
const x = get_handles_offset_left();
|
|
const y = get_handles_offset_top();
|
|
const grab_size = 32;
|
|
for ({ len_key, pos_key, region, offset } of [
|
|
{ len_key: "width", pos_key: "left", region: x_axis, offset: x },
|
|
{ len_key: "height", pos_key: "top", region: y_axis, offset: y },
|
|
]) {
|
|
let middle_start = Math.max(
|
|
rect[len_key] * magnification / 2 - grab_size / 2,
|
|
Math.min(
|
|
grab_size / 2,
|
|
rect[len_key] * magnification / 3
|
|
)
|
|
);
|
|
let middle_end = rect[len_key] * magnification - middle_start;
|
|
if (middle_end - middle_start < magnification) {
|
|
// give middle region min size of one (1) canvas pixel
|
|
middle_start = 0;
|
|
middle_end = magnification;
|
|
}
|
|
const start_start = -grab_size / 2;
|
|
const start_end = Math.min(
|
|
grab_size / 2,
|
|
middle_start
|
|
);
|
|
const end_start = rect[len_key] * magnification - start_end;
|
|
const end_end = rect[len_key] * magnification - start_start;
|
|
if (size_only) {
|
|
// For main canvas handles, where only the right/bottom handles are interactive,
|
|
// extend the middle regions left/up into the unused space of the useless handles.
|
|
// (This must be after middle_start is used above.)
|
|
middle_start = Math.max(-offset, Math.min(middle_start, middle_end - grab_size));
|
|
}
|
|
if (region === HANDLE_START) {
|
|
$h.css({ [pos_key]: offset - outset });
|
|
$grab_region.css({
|
|
[pos_key]: offset + start_start,
|
|
[len_key]: start_end - start_start,
|
|
});
|
|
} else if (region === HANDLE_MIDDLE) {
|
|
$h.css({ [pos_key]: offset + (rect[len_key] * magnification - hs) / 2 });
|
|
$grab_region.css({
|
|
[pos_key]: offset + middle_start,
|
|
[len_key]: middle_end - middle_start,
|
|
});
|
|
} else if (region === HANDLE_END) {
|
|
$h.css({ [pos_key]: offset + (rect[len_key] * magnification - hs / 2) });
|
|
$grab_region.css({
|
|
[pos_key]: offset + end_start,
|
|
[len_key]: end_end - end_start,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
$handles_container.on("update resize scroll", update_handle);
|
|
$G.on("resize theme-load", update_handle);
|
|
setTimeout(update_handle, 50);
|
|
|
|
handles.push($h[0], $grab_region[0]);
|
|
});
|
|
|
|
this.handles = handles;
|
|
|
|
// It shouldn't scroll when hiding/showing handles, so don't use jQuery hide/show or CSS display.
|
|
this.hide = () => { $(handles).css({ opacity: 0, pointerEvents: "none" }); };
|
|
this.show = () => { $(handles).css({ opacity: "", pointerEvents: "" }); };
|
|
}
|