jspaint/src/Handles.js

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: "" }); };
}