Wrap many modules in IIFEs to track their exports

main
Isaiah Odhner 2022-01-18 20:37:40 -05:00
parent 4fdd061507
commit 355fba3ee2
12 changed files with 3668 additions and 3579 deletions

View File

@ -1,143 +1,149 @@
((exports) => {
/** Used by the Colors Box and by the Edit Colors dialog */
function $Swatch(color) {
const $swatch = $(E("div")).addClass("swatch");
const swatch_canvas = make_canvas();
$(swatch_canvas).css({ pointerEvents: "none" }).appendTo($swatch);
/** Used by the Colors Box and by the Edit Colors dialog */
function $Swatch(color) {
const $swatch = $(E("div")).addClass("swatch");
const swatch_canvas = make_canvas();
$(swatch_canvas).css({ pointerEvents: "none" }).appendTo($swatch);
// @TODO: clean up event listener
$G.on("theme-load", () => { update_$swatch($swatch); });
$swatch.data("swatch", color);
update_$swatch($swatch, color);
// @TODO: clean up event listener
$G.on("theme-load", () => { update_$swatch($swatch); });
$swatch.data("swatch", color);
update_$swatch($swatch, color);
return $swatch;
}
function update_$swatch($swatch, new_color) {
if (new_color instanceof CanvasPattern) {
$swatch.addClass("pattern");
$swatch[0].dataset.color = "";
} else if (typeof new_color === "string") {
$swatch.removeClass("pattern");
$swatch[0].dataset.color = new_color;
} else if (new_color !== undefined) {
throw new TypeError(`argument to update must be CanvasPattern or string (or undefined); got type ${typeof new_color}`);
return $swatch;
}
new_color = new_color || $swatch.data("swatch");
$swatch.data("swatch", new_color);
const swatch_canvas = $swatch.find("canvas")[0];
requestAnimationFrame(() => {
swatch_canvas.width = $swatch.innerWidth();
swatch_canvas.height = $swatch.innerHeight();
if (new_color) {
swatch_canvas.ctx.fillStyle = new_color;
swatch_canvas.ctx.fillRect(0, 0, swatch_canvas.width, swatch_canvas.height);
function update_$swatch($swatch, new_color) {
if (new_color instanceof CanvasPattern) {
$swatch.addClass("pattern");
$swatch[0].dataset.color = "";
} else if (typeof new_color === "string") {
$swatch.removeClass("pattern");
$swatch[0].dataset.color = new_color;
} else if (new_color !== undefined) {
throw new TypeError(`argument to update must be CanvasPattern or string (or undefined); got type ${typeof new_color}`);
}
});
}
function $ColorBox(vertical) {
const $cb = $(E("div")).addClass("color-box");
const $current_colors = $Swatch(selected_colors.ternary).addClass("current-colors");
const $palette = $(E("div")).addClass("palette");
$cb.append($current_colors, $palette);
const $foreground_color = $Swatch(selected_colors.foreground).addClass("color-selection foreground-color");
const $background_color = $Swatch(selected_colors.background).addClass("color-selection background-color");
$current_colors.append($background_color, $foreground_color);
$G.on("option-changed", () => {
update_$swatch($foreground_color, selected_colors.foreground);
update_$swatch($background_color, selected_colors.background);
update_$swatch($current_colors, selected_colors.ternary);
});
$current_colors.on("pointerdown", () => {
const new_bg = selected_colors.foreground;
selected_colors.foreground = selected_colors.background;
selected_colors.background = new_bg;
$G.triggerHandler("option-changed");
});
const make_color_button = (color) => {
const $b = $Swatch(color).addClass("color-button");
$b.appendTo($palette);
const double_click_period_ms = 400;
let within_double_click_period = false;
let double_click_button = null;
let double_click_tid;
// @TODO: handle left+right click at same time
// can do this with mousedown instead of pointerdown, but may need to improve eye gaze mode click simulation
$b.on("pointerdown", e => {
// @TODO: allow metaKey for ternary color, and selection cropping, on macOS?
ctrl = e.ctrlKey;
button = e.button;
if (button === 0) {
$c.data("$last_fg_color_button", $b);
}
const color_selection_slot = ctrl ? "ternary" : button === 0 ? "foreground" : button === 2 ? "background" : null;
if (color_selection_slot) {
if (within_double_click_period && button === double_click_button) {
show_edit_colors_window($b, color_selection_slot);
} else {
selected_colors[color_selection_slot] = $b.data("swatch");
$G.trigger("option-changed");
}
clearTimeout(double_click_tid);
double_click_tid = setTimeout(() => {
within_double_click_period = false;
double_click_button = null;
}, double_click_period_ms);
within_double_click_period = true;
double_click_button = button;
new_color = new_color || $swatch.data("swatch");
$swatch.data("swatch", new_color);
const swatch_canvas = $swatch.find("canvas")[0];
requestAnimationFrame(() => {
swatch_canvas.width = $swatch.innerWidth();
swatch_canvas.height = $swatch.innerHeight();
if (new_color) {
swatch_canvas.ctx.fillStyle = new_color;
swatch_canvas.ctx.fillRect(0, 0, swatch_canvas.width, swatch_canvas.height);
}
});
};
const build_palette = () => {
$palette.empty();
palette.forEach(make_color_button);
// Note: this doesn't work until the colors box is in the DOM
const $some_button = $palette.find(".color-button");
if (vertical) {
const height_per_button =
$some_button.outerHeight() +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-top")) +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-bottom"));
$palette.height(Math.ceil(palette.length / 2) * height_per_button);
} else {
const width_per_button =
$some_button.outerWidth() +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-left")) +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-right"));
$palette.width(Math.ceil(palette.length / 2) * width_per_button);
}
// the "last foreground color button" starts out as the first in the palette
$c.data("$last_fg_color_button", $palette.find(".color-button:first-child"));
};
let $c;
if (vertical) {
$c = $Component(localize("Colors"), "colors-component", "tall", $cb);
$c.appendTo(get_direction() === "rtl" ? $left : $right); // opposite ToolBox by default
} else {
$c = $Component(localize("Colors"), "colors-component", "wide", $cb);
$c.appendTo($bottom);
}
build_palette();
$(window).on("theme-change", build_palette);
function $ColorBox(vertical) {
const $cb = $(E("div")).addClass("color-box");
$c.rebuild_palette = build_palette;
const $current_colors = $Swatch(selected_colors.ternary).addClass("current-colors");
const $palette = $(E("div")).addClass("palette");
return $c;
}
$cb.append($current_colors, $palette);
const $foreground_color = $Swatch(selected_colors.foreground).addClass("color-selection foreground-color");
const $background_color = $Swatch(selected_colors.background).addClass("color-selection background-color");
$current_colors.append($background_color, $foreground_color);
$G.on("option-changed", () => {
update_$swatch($foreground_color, selected_colors.foreground);
update_$swatch($background_color, selected_colors.background);
update_$swatch($current_colors, selected_colors.ternary);
});
$current_colors.on("pointerdown", () => {
const new_bg = selected_colors.foreground;
selected_colors.foreground = selected_colors.background;
selected_colors.background = new_bg;
$G.triggerHandler("option-changed");
});
const make_color_button = (color) => {
const $b = $Swatch(color).addClass("color-button");
$b.appendTo($palette);
const double_click_period_ms = 400;
let within_double_click_period = false;
let double_click_button = null;
let double_click_tid;
// @TODO: handle left+right click at same time
// can do this with mousedown instead of pointerdown, but may need to improve eye gaze mode click simulation
$b.on("pointerdown", e => {
// @TODO: allow metaKey for ternary color, and selection cropping, on macOS?
ctrl = e.ctrlKey;
button = e.button;
if (button === 0) {
$c.data("$last_fg_color_button", $b);
}
const color_selection_slot = ctrl ? "ternary" : button === 0 ? "foreground" : button === 2 ? "background" : null;
if (color_selection_slot) {
if (within_double_click_period && button === double_click_button) {
show_edit_colors_window($b, color_selection_slot);
} else {
selected_colors[color_selection_slot] = $b.data("swatch");
$G.trigger("option-changed");
}
clearTimeout(double_click_tid);
double_click_tid = setTimeout(() => {
within_double_click_period = false;
double_click_button = null;
}, double_click_period_ms);
within_double_click_period = true;
double_click_button = button;
}
});
};
const build_palette = () => {
$palette.empty();
palette.forEach(make_color_button);
// Note: this doesn't work until the colors box is in the DOM
const $some_button = $palette.find(".color-button");
if (vertical) {
const height_per_button =
$some_button.outerHeight() +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-top")) +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-bottom"));
$palette.height(Math.ceil(palette.length / 2) * height_per_button);
} else {
const width_per_button =
$some_button.outerWidth() +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-left")) +
parseFloat(getComputedStyle($some_button[0]).getPropertyValue("margin-right"));
$palette.width(Math.ceil(palette.length / 2) * width_per_button);
}
// the "last foreground color button" starts out as the first in the palette
$c.data("$last_fg_color_button", $palette.find(".color-button:first-child"));
};
let $c;
if (vertical) {
$c = $Component(localize("Colors"), "colors-component", "tall", $cb);
$c.appendTo(get_direction() === "rtl" ? $left : $right); // opposite ToolBox by default
} else {
$c = $Component(localize("Colors"), "colors-component", "wide", $cb);
$c.appendTo($bottom);
}
build_palette();
$(window).on("theme-change", build_palette);
$c.rebuild_palette = build_palette;
return $c;
}
exports.$ColorBox = $ColorBox;
exports.$Swatch = $Swatch;
exports.update_$swatch = update_$swatch;
})(window);

View File

@ -1,410 +1,414 @@
((exports) => {
// Segments here represent UI components as far as a layout algorithm is concerned,
// line segments in one dimension (regardless of whether that dimension is vertical or horizontal),
// with a reference to the UI component DOM element so it can be updated.
// Segments here represent UI components as far as a layout algorithm is concerned,
// line segments in one dimension (regardless of whether that dimension is vertical or horizontal),
// with a reference to the UI component DOM element so it can be updated.
function get_segments(component_area_el, pos_axis, exclude_component_el) {
const $other_components = $(component_area_el).find(".component").not(exclude_component_el);
return $other_components.toArray().map((component_el) => {
const segment = { element: component_el };
if (pos_axis === "top") {
segment.pos = component_el.offsetTop;
segment.length = component_el.clientHeight;
} else if (pos_axis === "left") {
segment.pos = component_el.offsetLeft;
segment.length = component_el.clientWidth;
} else if (pos_axis === "right") {
segment.pos = component_area_el.scrollWidth - component_el.offsetLeft;
segment.length = component_el.clientWidth;
}
return segment;
});
}
function adjust_segments(segments, total_available_length) {
segments.sort((a, b) => a.pos - b.pos);
// Clamp
for (const segment of segments) {
segment.pos = Math.max(segment.pos, 0);
segment.pos = Math.min(segment.pos, total_available_length - segment.length);
function get_segments(component_area_el, pos_axis, exclude_component_el) {
const $other_components = $(component_area_el).find(".component").not(exclude_component_el);
return $other_components.toArray().map((component_el) => {
const segment = { element: component_el };
if (pos_axis === "top") {
segment.pos = component_el.offsetTop;
segment.length = component_el.clientHeight;
} else if (pos_axis === "left") {
segment.pos = component_el.offsetLeft;
segment.length = component_el.clientWidth;
} else if (pos_axis === "right") {
segment.pos = component_area_el.scrollWidth - component_el.offsetLeft;
segment.length = component_el.clientWidth;
}
return segment;
});
}
// Shove things downwards to prevent overlap
for (let i = 1; i < segments.length; i++) {
const segment = segments[i];
const prev_segment = segments[i - 1];
const overlap = prev_segment.pos + prev_segment.length - segment.pos;
if (overlap > 0) {
segment.pos += overlap;
function adjust_segments(segments, total_available_length) {
segments.sort((a, b) => a.pos - b.pos);
// Clamp
for (const segment of segments) {
segment.pos = Math.max(segment.pos, 0);
segment.pos = Math.min(segment.pos, total_available_length - segment.length);
}
// Shove things downwards to prevent overlap
for (let i = 1; i < segments.length; i++) {
const segment = segments[i];
const prev_segment = segments[i - 1];
const overlap = prev_segment.pos + prev_segment.length - segment.pos;
if (overlap > 0) {
segment.pos += overlap;
}
}
// Clamp
for (const segment of segments) {
segment.pos = Math.max(segment.pos, 0);
segment.pos = Math.min(segment.pos, total_available_length - segment.length);
}
// Shove things upwards to get things back on screen
for (let i = segments.length - 2; i >= 0; i--) {
const segment = segments[i];
const prev_segment = segments[i + 1];
const overlap = segment.pos + segment.length - prev_segment.pos;
if (overlap > 0) {
segment.pos -= overlap;
}
}
}
// Clamp
for (const segment of segments) {
segment.pos = Math.max(segment.pos, 0);
segment.pos = Math.min(segment.pos, total_available_length - segment.length);
}
function apply_segments(component_area_el, pos_axis, segments) {
// Since things aren't positioned absolutely, calculate space between
let length_before = 0;
for (const segment of segments) {
segment.margin_before = segment.pos - length_before;
length_before = segment.length + segment.pos;
}
// Shove things upwards to get things back on screen
for (let i = segments.length - 2; i >= 0; i--) {
const segment = segments[i];
const prev_segment = segments[i + 1];
const overlap = segment.pos + segment.length - prev_segment.pos;
if (overlap > 0) {
segment.pos -= overlap;
// Apply to the DOM
for (const segment of segments) {
component_area_el.appendChild(segment.element);
$(segment.element).css(`margin-${pos_axis}`, segment.margin_before);
}
}
}
function apply_segments(component_area_el, pos_axis, segments) {
// Since things aren't positioned absolutely, calculate space between
let length_before = 0;
for (const segment of segments) {
segment.margin_before = segment.pos - length_before;
length_before = segment.length + segment.pos;
}
function $Component(title, className, orientation, $el) {
// A draggable widget that can be undocked into a window
const $c = $(E("div")).addClass("component");
$c.addClass(className);
$c.addClass(orientation);
$c.append($el);
$c.css("touch-action", "none");
// Apply to the DOM
for (const segment of segments) {
component_area_el.appendChild(segment.element);
$(segment.element).css(`margin-${pos_axis}`, segment.margin_before);
}
}
function $Component(title, className, orientation, $el) {
// A draggable widget that can be undocked into a window
const $c = $(E("div")).addClass("component");
$c.addClass(className);
$c.addClass(orientation);
$c.append($el);
$c.css("touch-action", "none");
const $w = new $ToolWindow($c);
$w.title(title);
$w.hide();
$w.$content.addClass({
tall: "vertical",
wide: "horizontal",
}[orientation]);
// Nudge the Colors component over a tiny bit
if (className === "colors-component" && orientation === "wide") {
$c.css("position", "relative");
$c.css(`margin-${get_direction() === "rtl" ? "right" : "left"}`, "3px");
}
let iid;
if ($("body").hasClass("eye-gaze-mode")) {
// @TODO: don't use an interval for this!
iid = setInterval(() => {
const scale = 3;
$c.css({
transform: `scale(${scale})`,
transformOrigin: "0 0",
marginRight: $c[0].scrollWidth * (scale - 1),
marginBottom: $c[0].scrollHeight * (scale - 1),
});
}, 200);
}
let ox, oy;
let ox2, oy2;
let w, h;
let pos = 0;
let pos_axis;
let last_docked_to_pos;
let $last_docked_to;
let $dock_to;
let $ghost;
if (orientation === "tall") {
pos_axis = "top";
} else if (get_direction() === "rtl") {
pos_axis = "right";
} else {
pos_axis = "left";
}
const dock_to = $dock_to => {
const $w = new $ToolWindow($c);
$w.title(title);
$w.hide();
$w.$content.addClass({
tall: "vertical",
wide: "horizontal",
}[orientation]);
// must get layout state *before* changing it
const segments = get_segments($dock_to[0], pos_axis, $c[0]);
// so we can measure clientWidth/clientHeight
$dock_to.append($c);
segments.push({
element: $c[0],
pos: pos,
length: $c[0][pos_axis === "top" ? "clientHeight" : "clientWidth"],
});
const total_available_length = pos_axis === "top" ? $dock_to.height() : $dock_to.width();
// console.log("before adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
adjust_segments(segments, total_available_length);
// console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
apply_segments($dock_to[0], pos_axis, segments);
// Save where it's now docked to
$last_docked_to = $dock_to;
last_docked_to_pos = pos;
};
const undock_to = (x, y) => {
const component_area_el = $c.closest(".component-area")[0];
// must get layout state *before* changing it
const segments = get_segments(component_area_el, pos_axis, $c[0]);
$c.css("position", "relative");
$c.css(`margin-${pos_axis}`, "");
// Put the component in the window
$w.$content.append($c);
// Show and position the window
$w.show();
$w.css({
left: x,
top: y,
});
const total_available_length = pos_axis === "top" ? $(component_area_el).height() : $(component_area_el).width();
// console.log("before adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
adjust_segments(segments, total_available_length);
// console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
apply_segments(component_area_el, pos_axis, segments);
};
$w.on("window-drag-start", (e) => {
e.preventDefault();
});
const imagine_window_dimensions = () => {
const prev_window_shown = $w.is(":visible");
$w.show();
let $spacer;
let { offsetLeft, offsetTop } = $c[0];
if ($c.closest(".tool-window").length == 0) {
const styles = getComputedStyle($c[0]);
$spacer = $(E("div")).addClass("component").css({
width: styles.width,
height: styles.height,
// don't copy margin, margin is actually used for positioning the components in the docking areas
// don't copy padding, padding changes based on whether the component is in a window in modern theme
// let padding be influenced by CSS
});
$w.append($spacer);
({ offsetLeft, offsetTop } = $spacer[0]);
// Nudge the Colors component over a tiny bit
if (className === "colors-component" && orientation === "wide") {
$c.css("position", "relative");
$c.css(`margin-${get_direction() === "rtl" ? "right" : "left"}`, "3px");
}
const rect = $w[0].getBoundingClientRect();
if ($spacer) {
$spacer.remove();
let iid;
if ($("body").hasClass("eye-gaze-mode")) {
// @TODO: don't use an interval for this!
iid = setInterval(() => {
const scale = 3;
$c.css({
transform: `scale(${scale})`,
transformOrigin: "0 0",
marginRight: $c[0].scrollWidth * (scale - 1),
marginBottom: $c[0].scrollHeight * (scale - 1),
});
}, 200);
}
if (!prev_window_shown) {
$w.hide();
}
const w_styles = getComputedStyle($w[0]);
offsetLeft += parseFloat(w_styles.borderLeftWidth);
offsetTop += parseFloat(w_styles.borderTopWidth);
return { rect, offsetLeft, offsetTop };
};
const imagine_docked_dimensions = ($dock_to = (pos_axis === "top" ? $left : $bottom)) => {
if ($c.closest(".tool-window").length == 0) {
return { rect: $c[0].getBoundingClientRect() };
}
const styles = getComputedStyle($c[0]);
const $spacer = $(E("div")).addClass("component").css({
width: styles.width,
height: styles.height,
flex: "0 0 auto",
});
$dock_to.prepend($spacer);
const rect = $spacer[0].getBoundingClientRect();
if ($spacer) {
$spacer.remove();
}
return { rect };
};
const render_ghost = (e) => {
const { rect } = $dock_to ? imagine_docked_dimensions($dock_to) : imagine_window_dimensions()
let ox, oy;
let ox2, oy2;
let w, h;
let pos = 0;
let pos_axis;
let last_docked_to_pos;
let $last_docked_to;
let $dock_to;
let $ghost;
// Make sure these dimensions are odd numbers
// so the alternating pattern of the border is unbroken
w = (~~(rect.width / 2)) * 2 + 1;
h = (~~(rect.height / 2)) * 2 + 1;
if (!$ghost) {
$ghost = $(E("div")).addClass("component-ghost dock");
$ghost.appendTo("body");
}
const inset = $dock_to ? 0 : 3;
$ghost.css({
position: "absolute",
display: "block",
width: w - inset * 2,
height: h - inset * 2,
left: e.clientX + ($dock_to ? ox : ox2) + inset,
top: e.clientY + ($dock_to ? oy : oy2) + inset,
});
if ($dock_to) {
$ghost.addClass("dock");
} else {
$ghost.removeClass("dock");
}
};
$c.add($w.$titlebar).on("pointerdown", e => {
// Only start a drag via a left click directly on the component element or titlebar
if (e.button !== 0) { return; }
const validTarget =
$c.is(e.target) ||
(
$(e.target).closest($w.$titlebar).length > 0 &&
$(e.target).closest("button").length === 0
);
if (!validTarget) { return; }
// Don't allow dragging in eye gaze mode
if ($("body").hasClass("eye-gaze-mode")) { return; }
const docked = imagine_docked_dimensions();
const rect = $c[0].getBoundingClientRect();
ox = rect.left - e.clientX;
oy = rect.top - e.clientY;
ox = -Math.min(Math.max(-ox, 0), docked.rect.width);
oy = -Math.min(Math.max(-oy, 0), docked.rect.height);
const { offsetLeft, offsetTop } = imagine_window_dimensions();
ox2 = rect.left - offsetLeft - e.clientX;
oy2 = rect.top - offsetTop - e.clientY;
$("body").addClass("dragging");
$("body").css({ cursor: "default" }).addClass("cursor-bully");
$G.on("pointermove", drag_update_position);
$G.one("pointerup", e => {
$G.off("pointermove", drag_update_position);
drag_onpointerup(e);
$("body").removeClass("dragging");
$("body").css({ cursor: "" }).removeClass("cursor-bully");
$canvas.trigger("pointerleave"); // prevent magnifier preview showing until you move the mouse
});
render_ghost(e);
drag_update_position(e);
// Prevent text selection anywhere within the component
e.preventDefault();
});
const drag_update_position = e => {
$ghost.css({
left: e.clientX + ox,
top: e.clientY + oy,
});
$dock_to = null;
const { width, height } = imagine_docked_dimensions().rect;
const dock_ghost_left = e.clientX + ox;
const dock_ghost_top = e.clientY + oy;
const dock_ghost_right = dock_ghost_left + width;
const dock_ghost_bottom = dock_ghost_top + height;
const q = 5;
if (orientation === "tall") {
pos_axis = "top";
if (dock_ghost_left - q < $left[0].getBoundingClientRect().right) {
$dock_to = $left;
}
if (dock_ghost_right + q > $right[0].getBoundingClientRect().left) {
$dock_to = $right;
}
} else if (get_direction() === "rtl") {
pos_axis = "right";
} else {
pos_axis = get_direction() === "rtl" ? "right" : "left";
if (dock_ghost_top - q < $top[0].getBoundingClientRect().bottom) {
$dock_to = $top;
}
if (dock_ghost_bottom + q > $bottom[0].getBoundingClientRect().top) {
$dock_to = $bottom;
}
pos_axis = "left";
}
if ($dock_to) {
const dock_to_rect = $dock_to[0].getBoundingClientRect();
pos = (
pos_axis === "top" ? dock_ghost_top : pos_axis === "right" ? dock_ghost_right : dock_ghost_left
) - dock_to_rect[pos_axis];
if (pos_axis === "right") {
pos *= -1;
}
}
const dock_to = $dock_to => {
$w.hide();
render_ghost(e);
// must get layout state *before* changing it
const segments = get_segments($dock_to[0], pos_axis, $c[0]);
e.preventDefault();
};
// so we can measure clientWidth/clientHeight
$dock_to.append($c);
const drag_onpointerup = e => {
segments.push({
element: $c[0],
pos: pos,
length: $c[0][pos_axis === "top" ? "clientHeight" : "clientWidth"],
});
$w.hide();
const total_available_length = pos_axis === "top" ? $dock_to.height() : $dock_to.width();
// console.log("before adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
adjust_segments(segments, total_available_length);
// console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
// If the component is docked to a component area (a side)
if ($c.parent().is(".component-area")) {
// Save where it's docked so we can dock back later
$last_docked_to = $c.parent();
if ($dock_to) {
last_docked_to_pos = pos;
}
}
apply_segments($dock_to[0], pos_axis, segments);
if ($dock_to) {
// Dock component to $dock_to
dock_to($dock_to);
} else {
undock_to(e.clientX + ox2, e.clientY + oy2);
}
// Save where it's now docked to
$last_docked_to = $dock_to;
last_docked_to_pos = pos;
};
const undock_to = (x, y) => {
const component_area_el = $c.closest(".component-area")[0];
// must get layout state *before* changing it
const segments = get_segments(component_area_el, pos_axis, $c[0]);
$ghost && $ghost.remove();
$ghost = null;
$c.css("position", "relative");
$c.css(`margin-${pos_axis}`, "");
$G.trigger("resize");
};
$c.dock = ($dock_to) => {
pos = last_docked_to_pos ?? 0;
dock_to($dock_to ?? $last_docked_to);
};
$c.undock_to = undock_to;
$c.show = () => {
$($c[0]).show(); // avoid recursion
if ($.contains($w[0], $c[0])) {
// Put the component in the window
$w.$content.append($c);
// Show and position the window
$w.show();
}
return $c;
};
$c.hide = () => {
$c.add($w).hide();
return $c;
};
$c.toggle = () => {
if ($c.is(":visible")) {
$c.hide();
} else {
$c.show();
}
return $c;
};
$c.destroy = () => {
$w.close();
$c.remove();
clearInterval(iid);
};
$w.css({
left: x,
top: y,
});
$w.on("close", e => {
e.preventDefault();
$w.hide();
});
const total_available_length = pos_axis === "top" ? $(component_area_el).height() : $(component_area_el).width();
// console.log("before adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
adjust_segments(segments, total_available_length);
// console.log("after adjustment", JSON.stringify(segments, (_key,val)=> (val instanceof Element) ? val.className : val));
apply_segments(component_area_el, pos_axis, segments);
};
return $c;
}
$w.on("window-drag-start", (e) => {
e.preventDefault();
});
const imagine_window_dimensions = () => {
const prev_window_shown = $w.is(":visible");
$w.show();
let $spacer;
let { offsetLeft, offsetTop } = $c[0];
if ($c.closest(".tool-window").length == 0) {
const styles = getComputedStyle($c[0]);
$spacer = $(E("div")).addClass("component").css({
width: styles.width,
height: styles.height,
// don't copy margin, margin is actually used for positioning the components in the docking areas
// don't copy padding, padding changes based on whether the component is in a window in modern theme
// let padding be influenced by CSS
});
$w.append($spacer);
({ offsetLeft, offsetTop } = $spacer[0]);
}
const rect = $w[0].getBoundingClientRect();
if ($spacer) {
$spacer.remove();
}
if (!prev_window_shown) {
$w.hide();
}
const w_styles = getComputedStyle($w[0]);
offsetLeft += parseFloat(w_styles.borderLeftWidth);
offsetTop += parseFloat(w_styles.borderTopWidth);
return { rect, offsetLeft, offsetTop };
};
const imagine_docked_dimensions = ($dock_to = (pos_axis === "top" ? $left : $bottom)) => {
if ($c.closest(".tool-window").length == 0) {
return { rect: $c[0].getBoundingClientRect() };
}
const styles = getComputedStyle($c[0]);
const $spacer = $(E("div")).addClass("component").css({
width: styles.width,
height: styles.height,
flex: "0 0 auto",
});
$dock_to.prepend($spacer);
const rect = $spacer[0].getBoundingClientRect();
if ($spacer) {
$spacer.remove();
}
return { rect };
};
const render_ghost = (e) => {
const { rect } = $dock_to ? imagine_docked_dimensions($dock_to) : imagine_window_dimensions()
// Make sure these dimensions are odd numbers
// so the alternating pattern of the border is unbroken
w = (~~(rect.width / 2)) * 2 + 1;
h = (~~(rect.height / 2)) * 2 + 1;
if (!$ghost) {
$ghost = $(E("div")).addClass("component-ghost dock");
$ghost.appendTo("body");
}
const inset = $dock_to ? 0 : 3;
$ghost.css({
position: "absolute",
display: "block",
width: w - inset * 2,
height: h - inset * 2,
left: e.clientX + ($dock_to ? ox : ox2) + inset,
top: e.clientY + ($dock_to ? oy : oy2) + inset,
});
if ($dock_to) {
$ghost.addClass("dock");
} else {
$ghost.removeClass("dock");
}
};
$c.add($w.$titlebar).on("pointerdown", e => {
// Only start a drag via a left click directly on the component element or titlebar
if (e.button !== 0) { return; }
const validTarget =
$c.is(e.target) ||
(
$(e.target).closest($w.$titlebar).length > 0 &&
$(e.target).closest("button").length === 0
);
if (!validTarget) { return; }
// Don't allow dragging in eye gaze mode
if ($("body").hasClass("eye-gaze-mode")) { return; }
const docked = imagine_docked_dimensions();
const rect = $c[0].getBoundingClientRect();
ox = rect.left - e.clientX;
oy = rect.top - e.clientY;
ox = -Math.min(Math.max(-ox, 0), docked.rect.width);
oy = -Math.min(Math.max(-oy, 0), docked.rect.height);
const { offsetLeft, offsetTop } = imagine_window_dimensions();
ox2 = rect.left - offsetLeft - e.clientX;
oy2 = rect.top - offsetTop - e.clientY;
$("body").addClass("dragging");
$("body").css({ cursor: "default" }).addClass("cursor-bully");
$G.on("pointermove", drag_update_position);
$G.one("pointerup", e => {
$G.off("pointermove", drag_update_position);
drag_onpointerup(e);
$("body").removeClass("dragging");
$("body").css({ cursor: "" }).removeClass("cursor-bully");
$canvas.trigger("pointerleave"); // prevent magnifier preview showing until you move the mouse
});
render_ghost(e);
drag_update_position(e);
// Prevent text selection anywhere within the component
e.preventDefault();
});
const drag_update_position = e => {
$ghost.css({
left: e.clientX + ox,
top: e.clientY + oy,
});
$dock_to = null;
const { width, height } = imagine_docked_dimensions().rect;
const dock_ghost_left = e.clientX + ox;
const dock_ghost_top = e.clientY + oy;
const dock_ghost_right = dock_ghost_left + width;
const dock_ghost_bottom = dock_ghost_top + height;
const q = 5;
if (orientation === "tall") {
pos_axis = "top";
if (dock_ghost_left - q < $left[0].getBoundingClientRect().right) {
$dock_to = $left;
}
if (dock_ghost_right + q > $right[0].getBoundingClientRect().left) {
$dock_to = $right;
}
} else {
pos_axis = get_direction() === "rtl" ? "right" : "left";
if (dock_ghost_top - q < $top[0].getBoundingClientRect().bottom) {
$dock_to = $top;
}
if (dock_ghost_bottom + q > $bottom[0].getBoundingClientRect().top) {
$dock_to = $bottom;
}
}
if ($dock_to) {
const dock_to_rect = $dock_to[0].getBoundingClientRect();
pos = (
pos_axis === "top" ? dock_ghost_top : pos_axis === "right" ? dock_ghost_right : dock_ghost_left
) - dock_to_rect[pos_axis];
if (pos_axis === "right") {
pos *= -1;
}
}
render_ghost(e);
e.preventDefault();
};
const drag_onpointerup = e => {
$w.hide();
// If the component is docked to a component area (a side)
if ($c.parent().is(".component-area")) {
// Save where it's docked so we can dock back later
$last_docked_to = $c.parent();
if ($dock_to) {
last_docked_to_pos = pos;
}
}
if ($dock_to) {
// Dock component to $dock_to
dock_to($dock_to);
} else {
undock_to(e.clientX + ox2, e.clientY + oy2);
}
$ghost && $ghost.remove();
$ghost = null;
$G.trigger("resize");
};
$c.dock = ($dock_to) => {
pos = last_docked_to_pos ?? 0;
dock_to($dock_to ?? $last_docked_to);
};
$c.undock_to = undock_to;
$c.show = () => {
$($c[0]).show(); // avoid recursion
if ($.contains($w[0], $c[0])) {
$w.show();
}
return $c;
};
$c.hide = () => {
$c.add($w).hide();
return $c;
};
$c.toggle = () => {
if ($c.is(":visible")) {
$c.hide();
} else {
$c.show();
}
return $c;
};
$c.destroy = () => {
$w.close();
$c.remove();
clearInterval(iid);
};
$w.on("close", e => {
e.preventDefault();
$w.hide();
});
return $c;
}
exports.$Component = $Component;
})(window);

View File

@ -1,93 +1,98 @@
((exports) => {
function $FontBox() {
const $fb = $(E("div")).addClass("font-box");
function $FontBox() {
const $fb = $(E("div")).addClass("font-box");
const $family = $(E("select")).addClass("inset-deep").attr({
"aria-label": "Font Family",
"aria-description": localize("Selects the font used by the text."),
});
const $size = $(E("input")).addClass("inset-deep").attr({
type: "number",
min: 8,
max: 72,
value: text_tool_font.size,
"aria-label": "Font Size",
"aria-description": localize("Selects the point size of the text."),
}).css({
maxWidth: 50,
});
const $button_group = $(E("span")).addClass("text-toolbar-button-group");
// @TODO: localized labels
const $bold = $Toggle(0, "bold", "Bold", localize("Sets or clears the text bold attribute."));
const $italic = $Toggle(1, "italic", "Italic", localize("Sets or clears the text italic attribute."));
const $underline = $Toggle(2, "underline", "Underline", localize("Sets or clears the text underline attribute."));
const $vertical = $Toggle(3, "vertical", "Vertical Writing Mode", localize("Only a Far East font can be used for vertical editing."));
$vertical.attr("disabled", true);
const $family = $(E("select")).addClass("inset-deep").attr({
"aria-label": "Font Family",
"aria-description": localize("Selects the font used by the text."),
});
const $size = $(E("input")).addClass("inset-deep").attr({
type: "number",
min: 8,
max: 72,
value: text_tool_font.size,
"aria-label": "Font Size",
"aria-description": localize("Selects the point size of the text."),
}).css({
maxWidth: 50,
});
const $button_group = $(E("span")).addClass("text-toolbar-button-group");
// @TODO: localized labels
const $bold = $Toggle(0, "bold", "Bold", localize("Sets or clears the text bold attribute."));
const $italic = $Toggle(1, "italic", "Italic", localize("Sets or clears the text italic attribute."));
const $underline = $Toggle(2, "underline", "Underline", localize("Sets or clears the text underline attribute."));
const $vertical = $Toggle(3, "vertical", "Vertical Writing Mode", localize("Only a Far East font can be used for vertical editing."));
$vertical.attr("disabled", true);
$button_group.append($bold, $italic, $underline, $vertical);
$fb.append($family, $size, $button_group);
$button_group.append($bold, $italic, $underline, $vertical);
$fb.append($family, $size, $button_group);
const update_font = () => {
text_tool_font.size = Number($size.val());
text_tool_font.family = $family.val();
$G.trigger("option-changed");
};
const update_font = () => {
text_tool_font.size = Number($size.val());
text_tool_font.family = $family.val();
$G.trigger("option-changed");
};
FontDetective.each(font => {
const $option = $(E("option"));
$option.val(font).text(font.name);
$family.append($option);
if (!text_tool_font.family) {
update_font();
FontDetective.each(font => {
const $option = $(E("option"));
$option.val(font).text(font.name);
$family.append($option);
if (!text_tool_font.family) {
update_font();
}
});
if (text_tool_font.family) {
$family.val(text_tool_font.family);
}
});
if (text_tool_font.family) {
$family.val(text_tool_font.family);
$family.on("change", update_font);
$size.on("change", update_font);
const $w = $ToolWindow();
$w.title(localize("Fonts"));
$w.$content.append($fb);
$w.center();
return $w;
function $Toggle(xi, thing, label, description) {
const $button = $(E("button")).addClass("toggle").attr({
"aria-pressed": false,
"aria-label": label,
"aria-description": description,
});
const $icon = $(E("span")).addClass("icon").appendTo($button);
$button.css({
width: 23,
height: 22,
padding: 0,
display: "inline-flex",
alignContent: "center",
alignItems: "center",
justifyContent: "center",
});
$icon.css({
flex: "0 0 auto",
display: "block",
width: 16,
height: 16,
"--icon-index": xi,
});
$button.on("click", () => {
$button.toggleClass("selected");
text_tool_font[thing] = $button.hasClass("selected");
$button.attr("aria-pressed", $button.hasClass("selected"));
update_font();
});
if (text_tool_font[thing]) {
$button.addClass("selected").attr("aria-pressed", true);
}
return $button;
}
}
$family.on("change", update_font);
$size.on("change", update_font);
exports.$FontBox = $FontBox;
const $w = $ToolWindow();
$w.title(localize("Fonts"));
$w.$content.append($fb);
$w.center();
return $w;
function $Toggle(xi, thing, label, description) {
const $button = $(E("button")).addClass("toggle").attr({
"aria-pressed": false,
"aria-label": label,
"aria-description": description,
});
const $icon = $(E("span")).addClass("icon").appendTo($button);
$button.css({
width: 23,
height: 22,
padding: 0,
display: "inline-flex",
alignContent: "center",
alignItems: "center",
justifyContent: "center",
});
$icon.css({
flex: "0 0 auto",
display: "block",
width: 16,
height: 16,
"--icon-index": xi,
});
$button.on("click", () => {
$button.toggleClass("selected");
text_tool_font[thing] = $button.hasClass("selected");
$button.attr("aria-pressed", $button.hasClass("selected"));
update_font();
});
if (text_tool_font[thing]) {
$button.addClass("selected").attr("aria-pressed", true);
}
return $button;
}
}
})(window);

View File

@ -1,122 +1,128 @@
let theme_dev_blob_url;
((exports) => {
function $ToolBox(tools, is_extras) {
const $tools = $(E("div")).addClass("tools");
const $tool_options = $(E("div")).addClass("tool-options");
let theme_dev_blob_url;
let showing_tooltips = false;
$tools.on("pointerleave", () => {
showing_tooltips = false;
$status_text.default();
});
function $ToolBox(tools, is_extras) {
const $tools = $(E("div")).addClass("tools");
const $tool_options = $(E("div")).addClass("tool-options");
const $buttons = $($.map(tools, (tool, i) => {
const $b = $(E("div")).addClass("tool");
$b.appendTo($tools);
tool.$button = $b;
$b.attr("title", tool.name);
const $icon = $(E("span")).addClass("tool-icon");
$icon.appendTo($b);
const update_css = () => {
const use_svg = !theme_dev_blob_url && (
(
(window.devicePixelRatio >= 3 || (window.devicePixelRatio % 1) !== 0)
) ||
$("body").hasClass("eye-gaze-mode")
);
$icon.css({
display: "block",
position: "absolute",
left: 4,
top: 4,
width: 16,
height: 16,
backgroundImage: theme_dev_blob_url ? `url(${theme_dev_blob_url})` : "",
"--icon-index": i,
});
$icon.toggleClass("use-svg", use_svg);
};
update_css();
$G.on("theme-load resize", update_css);
$b.on("click", e => {
if (e.shiftKey || e.ctrlKey) {
select_tool(tool, true);
return;
}
if (selected_tool === tool && tool.deselect) {
select_tools(return_to_tools);
} else {
select_tool(tool);
}
let showing_tooltips = false;
$tools.on("pointerleave", () => {
showing_tooltips = false;
$status_text.default();
});
$b.on("pointerenter", () => {
const show_tooltip = () => {
showing_tooltips = true;
$status_text.text(tool.description);
};
if (showing_tooltips) {
show_tooltip();
} else {
const tid = setTimeout(show_tooltip, 300);
$b.on("pointerleave", () => {
clearTimeout(tid);
const $buttons = $($.map(tools, (tool, i) => {
const $b = $(E("div")).addClass("tool");
$b.appendTo($tools);
tool.$button = $b;
$b.attr("title", tool.name);
const $icon = $(E("span")).addClass("tool-icon");
$icon.appendTo($b);
const update_css = () => {
const use_svg = !theme_dev_blob_url && (
(
(window.devicePixelRatio >= 3 || (window.devicePixelRatio % 1) !== 0)
) ||
$("body").hasClass("eye-gaze-mode")
);
$icon.css({
display: "block",
position: "absolute",
left: 4,
top: 4,
width: 16,
height: 16,
backgroundImage: theme_dev_blob_url ? `url(${theme_dev_blob_url})` : "",
"--icon-index": i,
});
}
});
$icon.toggleClass("use-svg", use_svg);
};
update_css();
$G.on("theme-load resize", update_css);
return $b[0];
}));
$b.on("click", e => {
if (e.shiftKey || e.ctrlKey) {
select_tool(tool, true);
return;
}
if (selected_tool === tool && tool.deselect) {
select_tools(return_to_tools);
} else {
select_tool(tool);
}
});
const $c = $Component(
is_extras ? "Extra Tools" : localize("Tools"),
is_extras ? "tools-component extra-tools-component" : "tools-component",
"tall",
$tools.add($tool_options)
);
$c.appendTo(get_direction() === "rtl" ? $right : $left); // opposite ColorBox by default
$c.update_selected_tool = () => {
$buttons.removeClass("selected");
selected_tools.forEach((selected_tool) => {
selected_tool.$button.addClass("selected");
});
$tool_options.children().detach();
$tool_options.append(selected_tool.$options);
$tool_options.children().trigger("update");
$canvas.css({
cursor: make_css_cursor(...selected_tool.cursor),
});
};
$c.update_selected_tool();
$b.on("pointerenter", () => {
const show_tooltip = () => {
showing_tooltips = true;
$status_text.text(tool.description);
};
if (showing_tooltips) {
show_tooltip();
} else {
const tid = setTimeout(show_tooltip, 300);
$b.on("pointerleave", () => {
clearTimeout(tid);
});
}
});
if (is_extras) {
$c.height(80);
return $b[0];
}));
const $c = $Component(
is_extras ? "Extra Tools" : localize("Tools"),
is_extras ? "tools-component extra-tools-component" : "tools-component",
"tall",
$tools.add($tool_options)
);
$c.appendTo(get_direction() === "rtl" ? $right : $left); // opposite ColorBox by default
$c.update_selected_tool = () => {
$buttons.removeClass("selected");
selected_tools.forEach((selected_tool) => {
selected_tool.$button.addClass("selected");
});
$tool_options.children().detach();
$tool_options.append(selected_tool.$options);
$tool_options.children().trigger("update");
$canvas.css({
cursor: make_css_cursor(...selected_tool.cursor),
});
};
$c.update_selected_tool();
if (is_extras) {
$c.height(80);
}
return $c;
}
return $c;
}
let dev_theme_tool_icons = false;
try {
dev_theme_tool_icons = localStorage.dev_theme_tool_icons === "true";
// eslint-disable-next-line no-empty
} catch (e) { }
if (dev_theme_tool_icons) {
let last_update_id = 0;
$G.on("session-update", () => {
last_update_id += 1;
const this_update_id = last_update_id;
main_canvas.toBlob((blob) => {
// avoid a race condition particularly when loading the document initially when the default canvas size is large, giving a larger PNG
if (this_update_id !== last_update_id) {
return;
}
URL.revokeObjectURL(theme_dev_blob_url);
theme_dev_blob_url = URL.createObjectURL(blob);
$G.triggerHandler("theme-load");
let dev_theme_tool_icons = false;
try {
dev_theme_tool_icons = localStorage.dev_theme_tool_icons === "true";
// eslint-disable-next-line no-empty
} catch (e) { }
if (dev_theme_tool_icons) {
let last_update_id = 0;
$G.on("session-update", () => {
last_update_id += 1;
const this_update_id = last_update_id;
main_canvas.toBlob((blob) => {
// avoid a race condition particularly when loading the document initially when the default canvas size is large, giving a larger PNG
if (this_update_id !== last_update_id) {
return;
}
URL.revokeObjectURL(theme_dev_blob_url);
theme_dev_blob_url = URL.createObjectURL(blob);
$G.triggerHandler("theme-load");
});
});
});
}
}
exports.$ToolBox = $ToolBox;
})(window);

View File

@ -1,103 +1,110 @@
((exports) => {
function make_window_supporting_scale(options) {
const $w = new $Window(options);
function make_window_supporting_scale(options) {
const $w = new $Window(options);
const scale_for_eye_gaze_mode_and_center = () => {
if (!$w.is(".edit-colors-window, .storage-manager, .attributes-window, .flip-and-rotate, .stretch-and-skew")) {
return;
}
const c = $w.$content[0];
const t = $w.$titlebar[0];
let scale = 1;
$w.$content.css({
transform: `scale(${scale})`,
transformOrigin: "0 0",
marginRight: "",
marginBottom: "",
});
if (document.body.classList.contains("eye-gaze-mode")) {
scale = Math.min(
(innerWidth) / c.offsetWidth,
(innerHeight - t.offsetHeight) / c.offsetHeight
);
const scale_for_eye_gaze_mode_and_center = () => {
if (!$w.is(".edit-colors-window, .storage-manager, .attributes-window, .flip-and-rotate, .stretch-and-skew")) {
return;
}
const c = $w.$content[0];
const t = $w.$titlebar[0];
let scale = 1;
$w.$content.css({
transform: `scale(${scale})`,
transformOrigin: "0 0",
marginRight: c.scrollWidth * (scale - 1),
});
// This is separate to prevent content going off the bottom of the window
// in case the layout changes due to text wrapping.
$w.$content.css({
marginBottom: c.scrollHeight * (scale - 1),
marginRight: "",
marginBottom: "",
});
if (document.body.classList.contains("eye-gaze-mode")) {
scale = Math.min(
(innerWidth) / c.offsetWidth,
(innerHeight - t.offsetHeight) / c.offsetHeight
);
$w.$content.css({
transform: `scale(${scale})`,
transformOrigin: "0 0",
marginRight: c.scrollWidth * (scale - 1),
});
// This is separate to prevent content going off the bottom of the window
// in case the layout changes due to text wrapping.
$w.$content.css({
marginBottom: c.scrollHeight * (scale - 1),
});
$w.center();
}
// for testing (WARNING: can cause rapid flashing, which can cause seizures):
// requestAnimationFrame(scale_for_eye_gaze_mode_and_center);
};
if (!options.$component) {
$w.center();
const scale_for_eye_gaze_mode_and_center_next_frame = () => {
requestAnimationFrame(scale_for_eye_gaze_mode_and_center);
};
const on_close = () => {
$w.off("close", on_close);
$G.off("eye-gaze-mode-toggled resize", scale_for_eye_gaze_mode_and_center_next_frame);
};
$w.on("close", on_close);
$G.on("eye-gaze-mode-toggled resize", scale_for_eye_gaze_mode_and_center_next_frame);
scale_for_eye_gaze_mode_and_center_next_frame();
}
// for testing (WARNING: can cause rapid flashing, which can cause seizures):
// requestAnimationFrame(scale_for_eye_gaze_mode_and_center);
};
if (!options.$component) {
$w.center();
if (options.$component) {
$w.$content.css({
contain: "none",
});
}
const scale_for_eye_gaze_mode_and_center_next_frame = () => {
requestAnimationFrame(scale_for_eye_gaze_mode_and_center);
};
const on_close = () => {
$w.off("close", on_close);
$G.off("eye-gaze-mode-toggled resize", scale_for_eye_gaze_mode_and_center_next_frame);
};
$w.on("close", on_close);
$G.on("eye-gaze-mode-toggled resize", scale_for_eye_gaze_mode_and_center_next_frame);
scale_for_eye_gaze_mode_and_center_next_frame();
return $w;
}
if (options.$component) {
$w.$content.css({
contain: "none",
function $ToolWindow($component) {
return make_window_supporting_scale({
$component,
toolWindow: true,
});
}
return $w;
}
function $ToolWindow($component) {
return make_window_supporting_scale({
$component,
toolWindow: true,
});
}
function $DialogWindow(title) {
const $w = make_window_supporting_scale({
title,
resizable: false,
maximizeButton: false,
minimizeButton: false,
// helpButton: @TODO
});
$w.addClass("dialog-window");
$w.$form = $(E("form")).appendTo($w.$content);
$w.$main = $(E("div")).appendTo($w.$form);
$w.$buttons = $(E("div")).appendTo($w.$form).addClass("button-group");
$w.$Button = (label, action) => {
const $b = $(E("button")).appendTo($w.$buttons).text(label);
$b.on("click", (e) => {
// prevent the form from submitting
// @TODO: instead, prevent the form's submit event
e.preventDefault();
action();
function $DialogWindow(title) {
const $w = make_window_supporting_scale({
title,
resizable: false,
maximizeButton: false,
minimizeButton: false,
// helpButton: @TODO
});
$w.addClass("dialog-window");
$b.on("pointerdown", () => {
$b.focus();
});
$w.$form = $(E("form")).appendTo($w.$content);
$w.$main = $(E("div")).appendTo($w.$form);
$w.$buttons = $(E("div")).appendTo($w.$form).addClass("button-group");
return $b;
};
$w.$Button = (label, action) => {
const $b = $(E("button")).appendTo($w.$buttons).text(label);
$b.on("click", (e) => {
// prevent the form from submitting
// @TODO: instead, prevent the form's submit event
e.preventDefault();
return $w;
}
action();
});
$b.on("pointerdown", () => {
$b.focus();
});
return $b;
};
return $w;
}
exports.$ToolWindow = $ToolWindow;
exports.$DialogWindow = $DialogWindow;
exports.make_window_supporting_scale = make_window_supporting_scale;
})(window);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,486 +1,490 @@
((exports) => {
let $help_window;
function show_help() {
if ($help_window) {
$help_window.focus();
return;
let $help_window;
function show_help() {
if ($help_window) {
$help_window.focus();
return;
}
$help_window = open_help_viewer({
title: localize("Paint Help"),
root: "help",
contentsFile: "help/mspaint.hhc",
}).$help_window;
$help_window.on("close", () => {
$help_window = null;
});
}
$help_window = open_help_viewer({
title: localize("Paint Help"),
root: "help",
contentsFile: "help/mspaint.hhc",
}).$help_window;
$help_window.on("close", () => {
$help_window = null;
});
}
// shared code with 98.js.org
// (copy-pasted / manually synced for now)
// shared code with 98.js.org
// (copy-pasted / manually synced for now)
function open_help_viewer(options) {
const $help_window = $Window({
title: options.title || "Help Topics",
icons: {
16: "images/chm-16x16.png",
},
resizable: true,
})
$help_window.addClass("help-window");
function open_help_viewer(options) {
const $help_window = $Window({
title: options.title || "Help Topics",
icons: {
16: "images/chm-16x16.png",
},
resizable: true,
})
$help_window.addClass("help-window");
let ignore_one_load = true;
let back_length = 0;
let forward_length = 0;
let ignore_one_load = true;
let back_length = 0;
let forward_length = 0;
const $main = $(E("div")).addClass("main");
const $toolbar = $(E("div")).addClass("toolbar");
const add_toolbar_button = (name, sprite_n, action_fn, enabled_fn) => {
const $button = $("<button class='lightweight'>")
.append($("<span>").text(name))
.appendTo($toolbar)
.on("click", () => {
action_fn();
});
$("<div class='icon'/>")
.appendTo($button)
.css({
backgroundPosition: `${-sprite_n * 55}px 0px`,
});
const update_enabled = () => {
$button[0].disabled = enabled_fn && !enabled_fn();
const $main = $(E("div")).addClass("main");
const $toolbar = $(E("div")).addClass("toolbar");
const add_toolbar_button = (name, sprite_n, action_fn, enabled_fn) => {
const $button = $("<button class='lightweight'>")
.append($("<span>").text(name))
.appendTo($toolbar)
.on("click", () => {
action_fn();
});
$("<div class='icon'/>")
.appendTo($button)
.css({
backgroundPosition: `${-sprite_n * 55}px 0px`,
});
const update_enabled = () => {
$button[0].disabled = enabled_fn && !enabled_fn();
};
update_enabled();
$help_window.on("click", "*", update_enabled);
$help_window.on("update-buttons", update_enabled);
return $button;
};
update_enabled();
$help_window.on("click", "*", update_enabled);
$help_window.on("update-buttons", update_enabled);
return $button;
};
const measure_sidebar_width = () =>
$contents.outerWidth() +
parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-left")) +
parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-right")) +
$resizer.outerWidth();
const $hide_button = add_toolbar_button("Hide", 0, () => {
const toggling_width = measure_sidebar_width();
$contents.hide();
$resizer.hide();
$hide_button.hide();
$show_button.show();
$help_window.width($help_window.width() - toggling_width);
$help_window.css("left", $help_window.offset().left + toggling_width);
$help_window.bringTitleBarInBounds();
});
const $show_button = add_toolbar_button("Show", 5, () => {
$contents.show();
$resizer.show();
$show_button.hide();
$hide_button.show();
const toggling_width = measure_sidebar_width();
$help_window.css("max-width", "unset");
$help_window.width($help_window.width() + toggling_width);
$help_window.css("left", $help_window.offset().left - toggling_width);
// $help_window.applyBounds() would push the window to fit (before trimming it only if needed)
// Trim the window to fit (especially for if maximized)
if ($help_window.offset().left < 0) {
$help_window.width($help_window.width() + $help_window.offset().left);
$help_window.css("left", 0);
}
$help_window.css("max-width", "");
}).hide();
add_toolbar_button("Back", 1, () => {
$iframe[0].contentWindow.history.back();
ignore_one_load = true;
back_length -= 1;
forward_length += 1;
}, () => back_length > 0);
add_toolbar_button("Forward", 2, () => {
$iframe[0].contentWindow.history.forward();
ignore_one_load = true;
forward_length -= 1;
back_length += 1;
}, () => forward_length > 0);
add_toolbar_button("Options", 3, () => { }, () => false); // @TODO: hotkey and underline on O
add_toolbar_button("Web Help", 4, () => {
iframe.src = "help/online_support.htm";
});
const $iframe = $Iframe({ src: "help/default.html" }).addClass("inset-deep");
const iframe = $iframe[0];
iframe.$window = $help_window; // for focus handling integration
const $resizer = $(E("div")).addClass("resizer");
const $contents = $(E("ul")).addClass("contents inset-deep");
// @TODO: fix race conditions
$iframe.on("load", () => {
if (!ignore_one_load) {
const measure_sidebar_width = () =>
$contents.outerWidth() +
parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-left")) +
parseFloat(getComputedStyle($contents[0]).getPropertyValue("margin-right")) +
$resizer.outerWidth();
const $hide_button = add_toolbar_button("Hide", 0, () => {
const toggling_width = measure_sidebar_width();
$contents.hide();
$resizer.hide();
$hide_button.hide();
$show_button.show();
$help_window.width($help_window.width() - toggling_width);
$help_window.css("left", $help_window.offset().left + toggling_width);
$help_window.bringTitleBarInBounds();
});
const $show_button = add_toolbar_button("Show", 5, () => {
$contents.show();
$resizer.show();
$show_button.hide();
$hide_button.show();
const toggling_width = measure_sidebar_width();
$help_window.css("max-width", "unset");
$help_window.width($help_window.width() + toggling_width);
$help_window.css("left", $help_window.offset().left - toggling_width);
// $help_window.applyBounds() would push the window to fit (before trimming it only if needed)
// Trim the window to fit (especially for if maximized)
if ($help_window.offset().left < 0) {
$help_window.width($help_window.width() + $help_window.offset().left);
$help_window.css("left", 0);
}
$help_window.css("max-width", "");
}).hide();
add_toolbar_button("Back", 1, () => {
$iframe[0].contentWindow.history.back();
ignore_one_load = true;
back_length -= 1;
forward_length += 1;
}, () => back_length > 0);
add_toolbar_button("Forward", 2, () => {
$iframe[0].contentWindow.history.forward();
ignore_one_load = true;
forward_length -= 1;
back_length += 1;
forward_length = 0;
}
// iframe.contentWindow.location.href
ignore_one_load = false;
$help_window.triggerHandler("update-buttons");
});
}, () => forward_length > 0);
add_toolbar_button("Options", 3, () => { }, () => false); // @TODO: hotkey and underline on O
add_toolbar_button("Web Help", 4, () => {
iframe.src = "help/online_support.htm";
});
$main.append($contents, $resizer, $iframe);
$help_window.$content.append($toolbar, $main);
const $iframe = $Iframe({ src: "help/default.html" }).addClass("inset-deep");
const iframe = $iframe[0];
iframe.$window = $help_window; // for focus handling integration
const $resizer = $(E("div")).addClass("resizer");
const $contents = $(E("ul")).addClass("contents inset-deep");
$help_window.css({ width: 800, height: 600 });
// @TODO: fix race conditions
$iframe.on("load", () => {
if (!ignore_one_load) {
back_length += 1;
forward_length = 0;
}
// iframe.contentWindow.location.href
ignore_one_load = false;
$help_window.triggerHandler("update-buttons");
});
$iframe.attr({ name: "help-frame" });
$iframe.css({
backgroundColor: "white",
border: "",
margin: "1px",
});
$contents.css({
margin: "1px",
});
$help_window.center();
$main.append($contents, $resizer, $iframe);
$help_window.$content.append($toolbar, $main);
$main.css({
position: "relative", // for resizer
});
$help_window.css({ width: 800, height: 600 });
const resizer_width = 4;
$resizer.css({
cursor: "ew-resize",
width: resizer_width,
boxSizing: "border-box",
background: "var(--ButtonFace)",
borderLeft: "1px solid var(--ButtonShadow)",
boxShadow: "inset 1px 0 0 var(--ButtonHilight)",
top: 0,
bottom: 0,
zIndex: 1,
});
$resizer.on("pointerdown", (e) => {
let pointermove, pointerup;
const getPos = (e) =>
Math.min($help_window.width() - 100, Math.max(20,
e.clientX - $help_window.$content.offset().left
));
$G.on("pointermove", pointermove = (e) => {
$resizer.css({
position: "absolute",
left: getPos(e)
$iframe.attr({ name: "help-frame" });
$iframe.css({
backgroundColor: "white",
border: "",
margin: "1px",
});
$contents.css({
margin: "1px",
});
$help_window.center();
$main.css({
position: "relative", // for resizer
});
const resizer_width = 4;
$resizer.css({
cursor: "ew-resize",
width: resizer_width,
boxSizing: "border-box",
background: "var(--ButtonFace)",
borderLeft: "1px solid var(--ButtonShadow)",
boxShadow: "inset 1px 0 0 var(--ButtonHilight)",
top: 0,
bottom: 0,
zIndex: 1,
});
$resizer.on("pointerdown", (e) => {
let pointermove, pointerup;
const getPos = (e) =>
Math.min($help_window.width() - 100, Math.max(20,
e.clientX - $help_window.$content.offset().left
));
$G.on("pointermove", pointermove = (e) => {
$resizer.css({
position: "absolute",
left: getPos(e)
});
$contents.css({
marginRight: resizer_width,
});
});
$contents.css({
marginRight: resizer_width,
$G.on("pointerup", pointerup = (e) => {
$G.off("pointermove", pointermove);
$G.off("pointerup", pointerup);
$resizer.css({
position: "",
left: ""
});
$contents.css({
flexBasis: getPos(e) - resizer_width,
marginRight: "",
});
});
});
$G.on("pointerup", pointerup = (e) => {
$G.off("pointermove", pointermove);
$G.off("pointerup", pointerup);
$resizer.css({
position: "",
left: ""
const parse_object_params = $object => {
// parse an $(<object>) to a plain object of key value pairs
const object = {};
for (const param of $object.children("param").get()) {
object[param.name] = param.value;
}
return object;
};
let $last_expanded;
const $Item = text => {
const $item = $(E("div")).addClass("item").text(text.trim());
$item.on("mousedown", () => {
$contents.find(".item").removeClass("selected");
$item.addClass("selected");
});
$contents.css({
flexBasis: getPos(e) - resizer_width,
marginRight: "",
});
});
});
const parse_object_params = $object => {
// parse an $(<object>) to a plain object of key value pairs
const object = {};
for (const param of $object.children("param").get()) {
object[param.name] = param.value;
}
return object;
};
let $last_expanded;
const $Item = text => {
const $item = $(E("div")).addClass("item").text(text.trim());
$item.on("mousedown", () => {
$contents.find(".item").removeClass("selected");
$item.addClass("selected");
});
$item.on("click", () => {
const $li = $item.parent();
if ($li.is(".folder")) {
if ($last_expanded) {
$last_expanded.not($li).removeClass("expanded");
$item.on("click", () => {
const $li = $item.parent();
if ($li.is(".folder")) {
if ($last_expanded) {
$last_expanded.not($li).removeClass("expanded");
}
$li.toggleClass("expanded");
$last_expanded = $li;
}
$li.toggleClass("expanded");
$last_expanded = $li;
}
});
return $item;
};
const $default_item_li = $(E("li")).addClass("page");
$default_item_li.append($Item("Welcome to Help").on("click", () => {
$iframe.attr({ src: "help/default.html" });
}));
$contents.append($default_item_li);
function renderItem(source_li, $folder_items_ul) {
const object = parse_object_params($(source_li).children("object"));
if ($(source_li).find("li").length > 0) {
const $folder_li = $(E("li")).addClass("folder");
$folder_li.append($Item(object.Name));
$contents.append($folder_li);
const $folder_items_ul = $(E("ul"));
$folder_li.append($folder_items_ul);
$(source_li).children("ul").children().get().forEach((li) => {
renderItem(li, $folder_items_ul);
});
} else {
const $item_li = $(E("li")).addClass("page");
$item_li.append($Item(object.Name).on("click", () => {
$iframe.attr({ src: `${options.root}/${object.Local}` });
}));
if ($folder_items_ul) {
$folder_items_ul.append($item_li);
return $item;
};
const $default_item_li = $(E("li")).addClass("page");
$default_item_li.append($Item("Welcome to Help").on("click", () => {
$iframe.attr({ src: "help/default.html" });
}));
$contents.append($default_item_li);
function renderItem(source_li, $folder_items_ul) {
const object = parse_object_params($(source_li).children("object"));
if ($(source_li).find("li").length > 0) {
const $folder_li = $(E("li")).addClass("folder");
$folder_li.append($Item(object.Name));
$contents.append($folder_li);
const $folder_items_ul = $(E("ul"));
$folder_li.append($folder_items_ul);
$(source_li).children("ul").children().get().forEach((li) => {
renderItem(li, $folder_items_ul);
});
} else {
$contents.append($item_li);
const $item_li = $(E("li")).addClass("page");
$item_li.append($Item(object.Name).on("click", () => {
$iframe.attr({ src: `${options.root}/${object.Local}` });
}));
if ($folder_items_ul) {
$folder_items_ul.append($item_li);
} else {
$contents.append($item_li);
}
}
}
}
fetch(options.contentsFile).then((response) => {
response.text().then((hhc) => {
$($.parseHTML(hhc)).filter("ul").children().get().forEach((li) => {
renderItem(li, null);
fetch(options.contentsFile).then((response) => {
response.text().then((hhc) => {
$($.parseHTML(hhc)).filter("ul").children().get().forEach((li) => {
renderItem(li, null);
});
}, (error) => {
show_error_message(`${localize("Failed to launch help.")} Failed to read ${options.contentsFile}.`, error);
});
}, (error) => {
show_error_message(`${localize("Failed to launch help.")} Failed to read ${options.contentsFile}.`, error);
});
}, (/* error */) => {
// access to error message is not allowed either, basically
if (location.protocol === "file:") {
showMessageBox({
// <p>${localize("Failed to launch help.")}</p>
// but it's already launched at this point
}, (/* error */) => {
// access to error message is not allowed either, basically
if (location.protocol === "file:") {
showMessageBox({
// <p>${localize("Failed to launch help.")}</p>
// but it's already launched at this point
// what's a good tutorial for starting a web server?
// https://gist.github.com/willurd/5720255 - impressive list, but not a tutorial
// https://attacomsian.com/blog/local-web-server - OK, good enough
messageHTML: `
// what's a good tutorial for starting a web server?
// https://gist.github.com/willurd/5720255 - impressive list, but not a tutorial
// https://attacomsian.com/blog/local-web-server - OK, good enough
messageHTML: `
<p>Help is not available when running from the <code>file:</code> protocol.</p>
<p>To use this feature, <a href="https://attacomsian.com/blog/local-web-server">start a web server</a>.</p>
`,
iconID: "error",
});
} else {
show_error_message(`${localize("Failed to launch help.")} ${localize("Access to %1 was denied.", options.contentsFile)}`);
}
});
// @TODO: keyboard accessability
// $help_window.on("keydown", (e)=> {
// switch(e.keyCode){
// case 37:
// show_error_message("MOVE IT");
// break;
// }
// });
// var task = new Task($help_window);
var task = {};
task.$help_window = $help_window;
return task;
}
var programs_being_loaded = 0;
function $Iframe(options) {
var $iframe = $("<iframe allowfullscreen sandbox='allow-same-origin allow-scripts allow-forms allow-pointer-lock allow-modals allow-popups allow-downloads'>");
var iframe = $iframe[0];
var disable_delegate_pointerup = false;
$iframe.focus_contents = function () {
if (!iframe.contentWindow) {
return;
}
if (iframe.contentDocument.hasFocus()) {
return;
}
disable_delegate_pointerup = true;
iframe.contentWindow.focus();
setTimeout(function () {
iframe.contentWindow.focus();
disable_delegate_pointerup = false;
iconID: "error",
});
} else {
show_error_message(`${localize("Failed to launch help.")} ${localize("Access to %1 was denied.", options.contentsFile)}`);
}
});
};
// Let the iframe to handle mouseup events outside itself
var delegate_pointerup = function () {
if (disable_delegate_pointerup) {
return;
}
// This try-catch may only be needed for running Cypress tests.
try {
if (iframe.contentWindow && iframe.contentWindow.jQuery) {
iframe.contentWindow.jQuery("body").trigger("pointerup");
}
if (iframe.contentWindow) {
const event = new iframe.contentWindow.MouseEvent("mouseup", { button: 0 });
iframe.contentWindow.dispatchEvent(event);
const event2 = new iframe.contentWindow.MouseEvent("mouseup", { button: 2 });
iframe.contentWindow.dispatchEvent(event2);
}
} catch (error) {
console.log("Failed to access iframe to delegate pointerup; got", error);
}
};
$G.on("mouseup blur", delegate_pointerup);
$iframe.destroy = () => {
$G.off("mouseup blur", delegate_pointerup);
};
// @TODO: delegate pointermove events too?
$("body").addClass("loading-program");
programs_being_loaded += 1;
$iframe.on("load", function () {
if (--programs_being_loaded <= 0) {
$("body").removeClass("loading-program");
}
// This try-catch may only be needed for running Cypress tests.
try {
if (window.themeCSSProperties) {
applyTheme(themeCSSProperties, iframe.contentDocument.documentElement);
}
// on Wayback Machine, and iframe's url not saved yet
if (iframe.contentDocument.querySelector("#error #livewebInfo.available")) {
var message = document.createElement("div");
message.style.position = "absolute";
message.style.left = "0";
message.style.right = "0";
message.style.top = "0";
message.style.bottom = "0";
message.style.background = "#c0c0c0";
message.style.color = "#000";
message.style.padding = "50px";
iframe.contentDocument.body.appendChild(message);
message.innerHTML = `<a target="_blank">Save this url in the Wayback Machine</a>`;
message.querySelector("a").href =
"https://web.archive.org/save/https://98.js.org/" +
iframe.src.replace(/.*https:\/\/98.js.org\/?/, "");
message.querySelector("a").style.color = "blue";
}
var $contentWindow = $(iframe.contentWindow);
$contentWindow.on("pointerdown click", function (e) {
iframe.$window && iframe.$window.focus();
// from close_menus in $MenuBar
$(".menu-button").trigger("release");
// Close any rogue floating submenus
$(".menu-popup").hide();
});
// We want to disable pointer events for other iframes, but not this one
$contentWindow.on("pointerdown", function (e) {
$iframe.css("pointer-events", "all");
$("body").addClass("dragging");
});
$contentWindow.on("pointerup", function (e) {
$("body").removeClass("dragging");
$iframe.css("pointer-events", "");
});
// $("iframe").css("pointer-events", ""); is called elsewhere.
// Otherwise iframes would get stuck in this interaction mode
iframe.contentWindow.close = function () {
iframe.$window && iframe.$window.close();
};
// @TODO: hook into saveAs (a la FileSaver.js) and another function for opening files
// iframe.contentWindow.saveAs = function(){
// saveAsDialog();
// };
} catch (error) {
console.log("Failed to reach into iframe; got", error);
}
});
if (options.src) {
$iframe.attr({ src: options.src });
// @TODO: keyboard accessability
// $help_window.on("keydown", (e)=> {
// switch(e.keyCode){
// case 37:
// show_error_message("MOVE IT");
// break;
// }
// });
// var task = new Task($help_window);
var task = {};
task.$help_window = $help_window;
return task;
}
$iframe.css({
minWidth: 0,
minHeight: 0, // overrides user agent styling apparently, fixes Sound Recorder
flex: 1,
border: 0, // overrides user agent styling
var programs_being_loaded = 0;
function $Iframe(options) {
var $iframe = $("<iframe allowfullscreen sandbox='allow-same-origin allow-scripts allow-forms allow-pointer-lock allow-modals allow-popups allow-downloads'>");
var iframe = $iframe[0];
var disable_delegate_pointerup = false;
$iframe.focus_contents = function () {
if (!iframe.contentWindow) {
return;
}
if (iframe.contentDocument.hasFocus()) {
return;
}
disable_delegate_pointerup = true;
iframe.contentWindow.focus();
setTimeout(function () {
iframe.contentWindow.focus();
disable_delegate_pointerup = false;
});
};
// Let the iframe to handle mouseup events outside itself
var delegate_pointerup = function () {
if (disable_delegate_pointerup) {
return;
}
// This try-catch may only be needed for running Cypress tests.
try {
if (iframe.contentWindow && iframe.contentWindow.jQuery) {
iframe.contentWindow.jQuery("body").trigger("pointerup");
}
if (iframe.contentWindow) {
const event = new iframe.contentWindow.MouseEvent("mouseup", { button: 0 });
iframe.contentWindow.dispatchEvent(event);
const event2 = new iframe.contentWindow.MouseEvent("mouseup", { button: 2 });
iframe.contentWindow.dispatchEvent(event2);
}
} catch (error) {
console.log("Failed to access iframe to delegate pointerup; got", error);
}
};
$G.on("mouseup blur", delegate_pointerup);
$iframe.destroy = () => {
$G.off("mouseup blur", delegate_pointerup);
};
// @TODO: delegate pointermove events too?
$("body").addClass("loading-program");
programs_being_loaded += 1;
$iframe.on("load", function () {
if (--programs_being_loaded <= 0) {
$("body").removeClass("loading-program");
}
// This try-catch may only be needed for running Cypress tests.
try {
if (window.themeCSSProperties) {
applyTheme(themeCSSProperties, iframe.contentDocument.documentElement);
}
// on Wayback Machine, and iframe's url not saved yet
if (iframe.contentDocument.querySelector("#error #livewebInfo.available")) {
var message = document.createElement("div");
message.style.position = "absolute";
message.style.left = "0";
message.style.right = "0";
message.style.top = "0";
message.style.bottom = "0";
message.style.background = "#c0c0c0";
message.style.color = "#000";
message.style.padding = "50px";
iframe.contentDocument.body.appendChild(message);
message.innerHTML = `<a target="_blank">Save this url in the Wayback Machine</a>`;
message.querySelector("a").href =
"https://web.archive.org/save/https://98.js.org/" +
iframe.src.replace(/.*https:\/\/98.js.org\/?/, "");
message.querySelector("a").style.color = "blue";
}
var $contentWindow = $(iframe.contentWindow);
$contentWindow.on("pointerdown click", function (e) {
iframe.$window && iframe.$window.focus();
// from close_menus in $MenuBar
$(".menu-button").trigger("release");
// Close any rogue floating submenus
$(".menu-popup").hide();
});
// We want to disable pointer events for other iframes, but not this one
$contentWindow.on("pointerdown", function (e) {
$iframe.css("pointer-events", "all");
$("body").addClass("dragging");
});
$contentWindow.on("pointerup", function (e) {
$("body").removeClass("dragging");
$iframe.css("pointer-events", "");
});
// $("iframe").css("pointer-events", ""); is called elsewhere.
// Otherwise iframes would get stuck in this interaction mode
iframe.contentWindow.close = function () {
iframe.$window && iframe.$window.close();
};
// @TODO: hook into saveAs (a la FileSaver.js) and another function for opening files
// iframe.contentWindow.saveAs = function(){
// saveAsDialog();
// };
} catch (error) {
console.log("Failed to reach into iframe; got", error);
}
});
if (options.src) {
$iframe.attr({ src: options.src });
}
$iframe.css({
minWidth: 0,
minHeight: 0, // overrides user agent styling apparently, fixes Sound Recorder
flex: 1,
border: 0, // overrides user agent styling
});
return $iframe;
}
// function $IframeWindow(options) {
// var $win = new $Window(options);
// var $iframe = $win.$iframe = $Iframe({ src: options.src });
// $win.$content.append($iframe);
// var iframe = $win.iframe = $iframe[0];
// // @TODO: should I instead of having iframe.$window, have something like get$Window?
// // Where all is $window needed?
// // I know it's used from within the iframe contents as frameElement.$window
// iframe.$window = $win;
// $win.on("close", function () {
// $iframe.destroy();
// });
// $win.onFocus($iframe.focus_contents);
// $iframe.on("load", function () {
// $win.show();
// $win.focus();
// // $iframe.focus_contents();
// });
// $win.setInnerDimensions = ({ width, height }) => {
// const width_from_frame = $win.width() - $win.$content.width();
// const height_from_frame = $win.height() - $win.$content.height();
// $win.css({
// width: width + width_from_frame,
// height: height + height_from_frame + 21,
// });
// };
// $win.setInnerDimensions({
// width: (options.innerWidth || 640),
// height: (options.innerHeight || 380),
// });
// $win.$content.css({
// display: "flex",
// flexDirection: "column",
// });
// // @TODO: cascade windows
// $win.center();
// $win.hide();
// return $win;
// }
// Fix dragging things (i.e. windows) over iframes (i.e. other windows)
// (when combined with a bit of css, .dragging iframe { pointer-events: none; })
// (and a similar thing in $IframeWindow)
$(window).on("pointerdown", function (e) {
//console.log(e.type);
$("body").addClass("dragging");
});
$(window).on("pointerup dragend blur", function (e) {
//console.log(e.type);
if (e.type === "blur") {
if (document.activeElement.tagName.match(/iframe/i)) {
return;
}
}
$("body").removeClass("dragging");
$("iframe").css("pointer-events", "");
});
return $iframe;
}
// function $IframeWindow(options) {
// var $win = new $Window(options);
// var $iframe = $win.$iframe = $Iframe({ src: options.src });
// $win.$content.append($iframe);
// var iframe = $win.iframe = $iframe[0];
// // @TODO: should I instead of having iframe.$window, have something like get$Window?
// // Where all is $window needed?
// // I know it's used from within the iframe contents as frameElement.$window
// iframe.$window = $win;
// $win.on("close", function () {
// $iframe.destroy();
// });
// $win.onFocus($iframe.focus_contents);
// $iframe.on("load", function () {
// $win.show();
// $win.focus();
// // $iframe.focus_contents();
// });
// $win.setInnerDimensions = ({ width, height }) => {
// const width_from_frame = $win.width() - $win.$content.width();
// const height_from_frame = $win.height() - $win.$content.height();
// $win.css({
// width: width + width_from_frame,
// height: height + height_from_frame + 21,
// });
// };
// $win.setInnerDimensions({
// width: (options.innerWidth || 640),
// height: (options.innerHeight || 380),
// });
// $win.$content.css({
// display: "flex",
// flexDirection: "column",
// });
// // @TODO: cascade windows
// $win.center();
// $win.hide();
// return $win;
// }
// Fix dragging things (i.e. windows) over iframes (i.e. other windows)
// (when combined with a bit of css, .dragging iframe { pointer-events: none; })
// (and a similar thing in $IframeWindow)
$(window).on("pointerdown", function (e) {
//console.log(e.type);
$("body").addClass("dragging");
});
$(window).on("pointerup dragend blur", function (e) {
//console.log(e.type);
if (e.type === "blur") {
if (document.activeElement.tagName.match(/iframe/i)) {
return;
}
}
$("body").removeClass("dragging");
$("iframe").css("pointer-events", "");
});
exports.show_help = show_help;
})(window);

View File

@ -1,266 +1,285 @@
((exports) => {
const TAU =
// //////|//////
// ///// | /////
// /// tau ///
// /// ...--> | <--... ///
// /// -' one | turn '- ///
// // .' | '. //
// // / | \ //
// // | | <-.. | //
// // | .->| \ | //
// // | / | | | //
- - - - - - - - Math.PI + Math.PI - - - - - 0;
// // // | \ | | | //
// // // | '->| / | //
// // // | | <-'' | //
// // // \ | / //
// // // '. | .' //
// // /// -. | .- ///
// // /// '''----|----''' ///
// // /// | ///
// // ////// | /////
// // //////|////// C/r;
const TAU =
// //////|//////
// ///// | /////
// /// tau ///
// /// ...--> | <--... ///
// /// -' one | turn '- ///
// // .' | '. //
// // / | \ //
// // | | <-.. | //
// // | .->| \ | //
// // | / | | | //
- - - - - - - - Math.PI + Math.PI - - - - - 0;
// // // | \ | | | //
// // // | '->| / | //
// // // | | <-'' | //
// // // \ | / //
// // // '. | .' //
// // /// -. | .- ///
// // /// '''----|----''' ///
// // /// | ///
// // ////// | /////
// // //////|////// C/r;
const is_pride_month = new Date().getMonth() === 5; // June (0-based, 0 is January)
const is_pride_month = new Date().getMonth() === 5; // June (0-based, 0 is January)
const $G = $(window);
const $G = $(window);
function make_css_cursor(name, coords, fallback) {
return `url(images/cursors/${name}.png) ${coords.join(" ")}, ${fallback}`;
}
function make_css_cursor(name, coords, fallback) {
return `url(images/cursors/${name}.png) ${coords.join(" ")}, ${fallback}`;
}
function E(t) {
return document.createElement(t);
}
function E(t) {
return document.createElement(t);
}
/** Returns a function, that, as long as it continues to be invoked, will not
be triggered. The function will be called after it stops being called for
N milliseconds. If `immediate` is passed, trigger the function on the
leading edge, instead of the trailing. */
function debounce(func, wait_ms, immediate) {
let timeout;
const debounced_func = function () {
const context = this;
const args = arguments;
/** Returns a function, that, as long as it continues to be invoked, will not
be triggered. The function will be called after it stops being called for
N milliseconds. If `immediate` is passed, trigger the function on the
leading edge, instead of the trailing. */
function debounce(func, wait_ms, immediate) {
let timeout;
const debounced_func = function () {
const context = this;
const args = arguments;
const later = () => {
timeout = null;
if (!immediate) {
const later = () => {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait_ms);
if (callNow) {
func.apply(context, args);
}
};
debounced_func.cancel = () => {
clearTimeout(timeout);
};
return debounced_func;
}
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait_ms);
if (callNow) {
func.apply(context, args);
}
};
debounced_func.cancel = () => {
clearTimeout(timeout);
};
return debounced_func;
}
function memoize_synchronous_function(func, max_entries = 50000) {
const cache = {};
const keys = [];
const memoized_func = (...args) => {
if (args.some((arg) => arg instanceof CanvasPattern)) {
return func.apply(null, args);
}
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
} else {
const val = func.apply(null, args);
cache[key] = val;
keys.push(key);
if (keys.length > max_entries) {
const oldest_key = keys.shift();
delete cache[oldest_key];
function memoize_synchronous_function(func, max_entries = 50000) {
const cache = {};
const keys = [];
const memoized_func = (...args) => {
if (args.some((arg) => arg instanceof CanvasPattern)) {
return func.apply(null, args);
}
return val;
}
}
memoized_func.clear_memo_cache = () => {
for (const key of keys) {
delete cache[key];
}
keys.length = 0;
};
return memoized_func;
}
window.get_rgba_from_color = memoize_synchronous_function((color) => {
const single_pixel_canvas = make_canvas(1, 1);
single_pixel_canvas.ctx.fillStyle = color;
single_pixel_canvas.ctx.fillRect(0, 0, 1, 1);
const image_data = single_pixel_canvas.ctx.getImageData(0, 0, 1, 1);
// We could just return image_data.data, but let's return an Array instead
// I'm not totally sure image_data.data wouldn't keep the ImageData object around in memory
return Array.from(image_data.data);
});
/**
* Compare two ImageData.
* Note: putImageData is lossy, due to premultiplied alpha.
* @returns {boolean} whether all pixels match within the specified threshold
*/
function image_data_match(a, b, threshold) {
const a_data = a.data;
const b_data = b.data;
if (a_data.length !== b_data.length) {
return false;
}
for (let len = a_data.length, i = 0; i < len; i++) {
if (a_data[i] !== b_data[i]) {
if (Math.abs(a_data[i] - b_data[i]) > threshold) {
return false;
const key = JSON.stringify(args);
if (cache[key]) {
return cache[key];
} else {
const val = func.apply(null, args);
cache[key] = val;
keys.push(key);
if (keys.length > max_entries) {
const oldest_key = keys.shift();
delete cache[oldest_key];
}
return val;
}
}
}
return true;
}
function make_canvas(width, height) {
const image = width;
const new_canvas = E("canvas");
const new_ctx = new_canvas.getContext("2d");
new_canvas.ctx = new_ctx;
new_ctx.disable_image_smoothing = () => {
new_ctx.imageSmoothingEnabled = false;
// condition is to avoid a deprecation warning in Firefox
if (new_ctx.imageSmoothingEnabled !== false) {
new_ctx.mozImageSmoothingEnabled = false;
new_ctx.webkitImageSmoothingEnabled = false;
new_ctx.msImageSmoothingEnabled = false;
}
};
new_ctx.enable_image_smoothing = () => {
new_ctx.imageSmoothingEnabled = true;
if (new_ctx.imageSmoothingEnabled !== true) {
new_ctx.mozImageSmoothingEnabled = true;
new_ctx.webkitImageSmoothingEnabled = true;
new_ctx.msImageSmoothingEnabled = true;
}
};
// @TODO: simplify the abstraction by defining setters for width/height
// that reset the image smoothing to disabled
// and make image smoothing a parameter to make_canvas
new_ctx.copy = image => {
new_canvas.width = image.naturalWidth || image.width;
new_canvas.height = image.naturalHeight || image.height;
// setting width/height resets image smoothing (along with everything)
new_ctx.disable_image_smoothing();
if (image instanceof ImageData) {
new_ctx.putImageData(image, 0, 0);
} else {
new_ctx.drawImage(image, 0, 0);
}
};
if (width && height) {
// make_canvas(width, height)
new_canvas.width = width;
new_canvas.height = height;
// setting width/height resets image smoothing (along with everything)
new_ctx.disable_image_smoothing();
} else if (image) {
// make_canvas(image)
new_ctx.copy(image);
memoized_func.clear_memo_cache = () => {
for (const key of keys) {
delete cache[key];
}
keys.length = 0;
};
return memoized_func;
}
return new_canvas;
}
const get_rgba_from_color = memoize_synchronous_function((color) => {
const single_pixel_canvas = make_canvas(1, 1);
function get_help_folder_icon(file_name) {
const icon_img = new Image();
icon_img.src = `help/${file_name}`;
return icon_img;
}
single_pixel_canvas.ctx.fillStyle = color;
single_pixel_canvas.ctx.fillRect(0, 0, 1, 1);
function get_icon_for_tool(tool) {
return get_help_folder_icon(tool.help_icon);
}
const image_data = single_pixel_canvas.ctx.getImageData(0, 0, 1, 1);
// not to be confused with load_image_from_uri
function load_image_simple(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => { resolve(img); };
img.onerror = () => { reject(new Error(`failed to load image from ${src}`)); };
img.src = src;
// We could just return image_data.data, but let's return an Array instead
// I'm not totally sure image_data.data wouldn't keep the ImageData object around in memory
return Array.from(image_data.data);
});
}
function get_icon_for_tools(tools) {
if (tools.length === 1) {
return get_icon_for_tool(tools[0]);
/**
* Compare two ImageData.
* Note: putImageData is lossy, due to premultiplied alpha.
* @returns {boolean} whether all pixels match within the specified threshold
*/
function image_data_match(a, b, threshold) {
const a_data = a.data;
const b_data = b.data;
if (a_data.length !== b_data.length) {
return false;
}
for (let len = a_data.length, i = 0; i < len; i++) {
if (a_data[i] !== b_data[i]) {
if (Math.abs(a_data[i] - b_data[i]) > threshold) {
return false;
}
}
}
return true;
}
const icon_canvas = make_canvas(16, 16);
Promise.all(tools.map((tool) => load_image_simple(`help/${tool.help_icon}`)))
.then((icons) => {
icons.forEach((icon, i) => {
const w = icon_canvas.width / icons.length;
const x = i * w;
const h = icon_canvas.height;
const y = 0;
icon_canvas.ctx.drawImage(icon, x, y, w, h, x, y, w, h);
});
})
return icon_canvas;
}
function make_canvas(width, height) {
const image = width;
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
*/
function rgb_to_hsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const new_canvas = E("canvas");
const new_ctx = new_canvas.getContext("2d");
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
new_canvas.ctx = new_ctx;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
new_ctx.disable_image_smoothing = () => {
new_ctx.imageSmoothingEnabled = false;
// condition is to avoid a deprecation warning in Firefox
if (new_ctx.imageSmoothingEnabled !== false) {
new_ctx.mozImageSmoothingEnabled = false;
new_ctx.webkitImageSmoothingEnabled = false;
new_ctx.msImageSmoothingEnabled = false;
}
};
new_ctx.enable_image_smoothing = () => {
new_ctx.imageSmoothingEnabled = true;
if (new_ctx.imageSmoothingEnabled !== true) {
new_ctx.mozImageSmoothingEnabled = true;
new_ctx.webkitImageSmoothingEnabled = true;
new_ctx.msImageSmoothingEnabled = true;
}
};
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
// @TODO: simplify the abstraction by defining setters for width/height
// that reset the image smoothing to disabled
// and make image smoothing a parameter to make_canvas
new_ctx.copy = image => {
new_canvas.width = image.naturalWidth || image.width;
new_canvas.height = image.naturalHeight || image.height;
// setting width/height resets image smoothing (along with everything)
new_ctx.disable_image_smoothing();
if (image instanceof ImageData) {
new_ctx.putImageData(image, 0, 0);
} else {
new_ctx.drawImage(image, 0, 0);
}
};
if (width && height) {
// make_canvas(width, height)
new_canvas.width = width;
new_canvas.height = height;
// setting width/height resets image smoothing (along with everything)
new_ctx.disable_image_smoothing();
} else if (image) {
// make_canvas(image)
new_ctx.copy(image);
}
h /= 6;
return new_canvas;
}
return [h, s, l];
}
function get_help_folder_icon(file_name) {
const icon_img = new Image();
icon_img.src = `help/${file_name}`;
return icon_img;
}
function get_icon_for_tool(tool) {
return get_help_folder_icon(tool.help_icon);
}
// not to be confused with load_image_from_uri
function load_image_simple(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => { resolve(img); };
img.onerror = () => { reject(new Error(`failed to load image from ${src}`)); };
img.src = src;
});
}
function get_icon_for_tools(tools) {
if (tools.length === 1) {
return get_icon_for_tool(tools[0]);
}
const icon_canvas = make_canvas(16, 16);
Promise.all(tools.map((tool) => load_image_simple(`help/${tool.help_icon}`)))
.then((icons) => {
icons.forEach((icon, i) => {
const w = icon_canvas.width / icons.length;
const x = i * w;
const h = icon_canvas.height;
const y = 0;
icon_canvas.ctx.drawImage(icon, x, y, w, h, x, y, w, h);
});
})
return icon_canvas;
}
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSL representation
*/
function rgb_to_hsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
exports.TAU = TAU;
exports.is_pride_month = is_pride_month;
exports.$G = $G;
exports.E = E;
exports.make_css_cursor = make_css_cursor;
exports.make_canvas = make_canvas;
exports.get_help_folder_icon = get_help_folder_icon;
exports.get_icon_for_tool = get_icon_for_tool;
exports.get_icon_for_tools = get_icon_for_tools;
exports.load_image_simple = load_image_simple;
exports.rgb_to_hsl = rgb_to_hsl;
exports.image_data_match = image_data_match;
exports.get_rgba_from_color = get_rgba_from_color;
exports.memoize_synchronous_function = memoize_synchronous_function;
exports.debounce = debounce;
})(window);

View File

@ -1,191 +1,197 @@
let $imgur_window;
((exports) => {
function show_imgur_uploader(blob) {
if ($imgur_window) {
$imgur_window.close();
}
$imgur_window = $DialogWindow().title("Upload To Imgur").addClass("horizontal-buttons");
let $imgur_window;
const $preview_image_area = $(E("div")).appendTo($imgur_window.$main).addClass("inset-deep");
const $imgur_url_area = $(E("div")).appendTo($imgur_window.$main);
const $imgur_status = $(E("div")).appendTo($imgur_window.$main);
// @TODO: maybe make this preview small but zoomable to full size?
// (starting small (max-width: 100%) and toggling to either scrollable or fullscreen)
// it should be clear that it's not going to upload a downsized version of your image
const $preview_image = $(E("img")).appendTo($preview_image_area).css({
display: "block", // prevent margin below due to inline display (vertical-align can also be used)
});
const blob_url = URL.createObjectURL(blob);
$preview_image.attr({ src: blob_url });
// $preview_image.css({maxWidth: "100%", maxHeight: "400px"});
$preview_image_area.css({
maxWidth: "90vw",
maxHeight: "70vh",
overflow: "auto",
marginBottom: "0.5em",
});
$preview_image.on("load", () => {
$imgur_window.css({ width: "auto" });
$imgur_window.center();
});
$imgur_window.on("close", () => {
URL.revokeObjectURL(blob_url);
});
const $upload_button = $imgur_window.$Button("Upload", () => {
URL.revokeObjectURL(blob_url);
$preview_image_area.remove();
$upload_button.remove();
$cancel_button.remove(); // @TODO: allow canceling upload request
$imgur_window.$content.width(300);
$imgur_window.center();
const $progress = $(E("progress")).appendTo($imgur_window.$main).addClass("inset-deep");
const $progress_percent = $(E("span")).appendTo($imgur_window.$main).css({
width: "2.3em",
display: "inline-block",
textAlign: "center",
});
const parseImgurResponseJSON = responseJSON => {
try {
return JSON.parse(responseJSON);
} catch (error) {
$imgur_status.text("Received an invalid JSON response from Imgur: ");
// .append($(E("pre")).text(responseJSON));
// show_error_message("Received an invalid JSON response from Imgur: ", responseJSON);
// show_error_message("Received an invalid JSON response from Imgur: ", responseJSON, but also error);
// $imgur_window.close();
// @TODO: DRY, including with show_error_message
$(E("pre"))
.appendTo($imgur_status)
.text(responseJSON)
.css({
background: "white",
color: "#333",
fontFamily: "monospace",
width: "500px",
overflow: "auto",
});
$(E("pre"))
.appendTo($imgur_status)
.text(error.toString())
.css({
background: "white",
color: "#333",
fontFamily: "monospace",
width: "500px",
overflow: "auto",
});
$imgur_window.css({ width: "auto" });
$imgur_window.center();
}
};
// make an HTTP request to the Imgur image upload API
const req = new XMLHttpRequest();
if (req.upload) {
req.upload.addEventListener('progress', event => {
if (event.lengthComputable) {
const progress_value = event.loaded / event.total;
const percentage_text = `${Math.floor(progress_value * 100)}%`;
$progress.val(progress_value);
$progress_percent.text(percentage_text);
}
}, false);
function show_imgur_uploader(blob) {
if ($imgur_window) {
$imgur_window.close();
}
$imgur_window = $DialogWindow().title("Upload To Imgur").addClass("horizontal-buttons");
req.addEventListener("readystatechange", () => {
if (req.readyState == 4 && req.status == 200) {
$progress.add($progress_percent).remove();
const $preview_image_area = $(E("div")).appendTo($imgur_window.$main).addClass("inset-deep");
const $imgur_url_area = $(E("div")).appendTo($imgur_window.$main);
const $imgur_status = $(E("div")).appendTo($imgur_window.$main);
const response = parseImgurResponseJSON(req.responseText);
if (!response) return;
if (!response.success) {
$imgur_status.text("Failed to upload image :(");
return;
}
const url = response.data.link;
$imgur_status.text("");
const $imgur_url = $(E("a")).attr({ id: "imgur-url", target: "_blank" });
$imgur_url.text(url);
$imgur_url.attr('href', url);
$imgur_url_area.append(
"<label>URL: </label>"
).append($imgur_url);
// @TODO: a button to copy the URL to the clipboard
// (also maybe put the URL in a readonly input)
let $ok_button;
const $delete_button = $imgur_window.$Button("Delete", () => {
const req = new XMLHttpRequest();
$delete_button[0].disabled = true;
req.addEventListener("readystatechange", () => {
if (req.readyState == 4 && req.status == 200) {
$delete_button.remove();
$ok_button.focus();
const response = parseImgurResponseJSON(req.responseText);
if (!response) return;
if (response.success) {
$imgur_url_area.remove();
$imgur_status.text("Deleted successfully");
} else {
$imgur_status.text("Failed to delete image :(");
}
} else if (req.readyState == 4) {
$imgur_status.text("Error deleting image :(");
$delete_button[0].disabled = false;
$delete_button.focus();
}
});
req.open("DELETE", `https://api.imgur.com/3/image/${response.data.deletehash}`, true);
req.setRequestHeader("Authorization", "Client-ID 203da2f300125a1");
req.setRequestHeader("Accept", "application/json");
req.send(null);
$imgur_status.text("Deleting...");
});
$ok_button = $imgur_window.$Button(localize("OK"), () => {
$imgur_window.close();
}).focus();
} else if (req.readyState == 4) {
$progress.add($progress_percent).remove();
$imgur_status.text("Error uploading image :(");
}
// @TODO: maybe make this preview small but zoomable to full size?
// (starting small (max-width: 100%) and toggling to either scrollable or fullscreen)
// it should be clear that it's not going to upload a downsized version of your image
const $preview_image = $(E("img")).appendTo($preview_image_area).css({
display: "block", // prevent margin below due to inline display (vertical-align can also be used)
});
const blob_url = URL.createObjectURL(blob);
$preview_image.attr({ src: blob_url });
// $preview_image.css({maxWidth: "100%", maxHeight: "400px"});
$preview_image_area.css({
maxWidth: "90vw",
maxHeight: "70vh",
overflow: "auto",
marginBottom: "0.5em",
});
$preview_image.on("load", () => {
$imgur_window.css({ width: "auto" });
$imgur_window.center();
});
$imgur_window.on("close", () => {
URL.revokeObjectURL(blob_url);
});
req.open("POST", "https://api.imgur.com/3/image", true);
const $upload_button = $imgur_window.$Button("Upload", () => {
const form_data = new FormData();
form_data.append("image", blob);
URL.revokeObjectURL(blob_url);
$preview_image_area.remove();
$upload_button.remove();
$cancel_button.remove(); // @TODO: allow canceling upload request
req.setRequestHeader("Authorization", "Client-ID 203da2f300125a1");
req.setRequestHeader("Accept", "application/json");
req.send(form_data);
$imgur_window.$content.width(300);
$imgur_window.center();
$imgur_status.text("Uploading...");
}).focus();
const $cancel_button = $imgur_window.$Button(localize("Cancel"), () => {
$imgur_window.close();
});
$imgur_window.$content.css({
width: "min(1000px, 80vw)",
});
$imgur_window.center();
}
const $progress = $(E("progress")).appendTo($imgur_window.$main).addClass("inset-deep");
const $progress_percent = $(E("span")).appendTo($imgur_window.$main).css({
width: "2.3em",
display: "inline-block",
textAlign: "center",
});
const parseImgurResponseJSON = responseJSON => {
try {
return JSON.parse(responseJSON);
} catch (error) {
$imgur_status.text("Received an invalid JSON response from Imgur: ");
// .append($(E("pre")).text(responseJSON));
// show_error_message("Received an invalid JSON response from Imgur: ", responseJSON);
// show_error_message("Received an invalid JSON response from Imgur: ", responseJSON, but also error);
// $imgur_window.close();
// @TODO: DRY, including with show_error_message
$(E("pre"))
.appendTo($imgur_status)
.text(responseJSON)
.css({
background: "white",
color: "#333",
fontFamily: "monospace",
width: "500px",
overflow: "auto",
});
$(E("pre"))
.appendTo($imgur_status)
.text(error.toString())
.css({
background: "white",
color: "#333",
fontFamily: "monospace",
width: "500px",
overflow: "auto",
});
$imgur_window.css({ width: "auto" });
$imgur_window.center();
}
};
// make an HTTP request to the Imgur image upload API
const req = new XMLHttpRequest();
if (req.upload) {
req.upload.addEventListener('progress', event => {
if (event.lengthComputable) {
const progress_value = event.loaded / event.total;
const percentage_text = `${Math.floor(progress_value * 100)}%`;
$progress.val(progress_value);
$progress_percent.text(percentage_text);
}
}, false);
}
req.addEventListener("readystatechange", () => {
if (req.readyState == 4 && req.status == 200) {
$progress.add($progress_percent).remove();
const response = parseImgurResponseJSON(req.responseText);
if (!response) return;
if (!response.success) {
$imgur_status.text("Failed to upload image :(");
return;
}
const url = response.data.link;
$imgur_status.text("");
const $imgur_url = $(E("a")).attr({ id: "imgur-url", target: "_blank" });
$imgur_url.text(url);
$imgur_url.attr('href', url);
$imgur_url_area.append(
"<label>URL: </label>"
).append($imgur_url);
// @TODO: a button to copy the URL to the clipboard
// (also maybe put the URL in a readonly input)
let $ok_button;
const $delete_button = $imgur_window.$Button("Delete", () => {
const req = new XMLHttpRequest();
$delete_button[0].disabled = true;
req.addEventListener("readystatechange", () => {
if (req.readyState == 4 && req.status == 200) {
$delete_button.remove();
$ok_button.focus();
const response = parseImgurResponseJSON(req.responseText);
if (!response) return;
if (response.success) {
$imgur_url_area.remove();
$imgur_status.text("Deleted successfully");
} else {
$imgur_status.text("Failed to delete image :(");
}
} else if (req.readyState == 4) {
$imgur_status.text("Error deleting image :(");
$delete_button[0].disabled = false;
$delete_button.focus();
}
});
req.open("DELETE", `https://api.imgur.com/3/image/${response.data.deletehash}`, true);
req.setRequestHeader("Authorization", "Client-ID 203da2f300125a1");
req.setRequestHeader("Accept", "application/json");
req.send(null);
$imgur_status.text("Deleting...");
});
$ok_button = $imgur_window.$Button(localize("OK"), () => {
$imgur_window.close();
}).focus();
} else if (req.readyState == 4) {
$progress.add($progress_percent).remove();
$imgur_status.text("Error uploading image :(");
}
});
req.open("POST", "https://api.imgur.com/3/image", true);
const form_data = new FormData();
form_data.append("image", blob);
req.setRequestHeader("Authorization", "Client-ID 203da2f300125a1");
req.setRequestHeader("Accept", "application/json");
req.send(form_data);
$imgur_status.text("Uploading...");
}).focus();
const $cancel_button = $imgur_window.$Button(localize("Cancel"), () => {
$imgur_window.close();
});
$imgur_window.$content.css({
width: "min(1000px, 80vw)",
});
$imgur_window.center();
}
exports.show_imgur_uploader = show_imgur_uploader;
})(window);

View File

@ -1,115 +1,121 @@
((exports) => {
let $storage_manager;
let $quota_exceeded_window;
let ignoring_quota_exceeded = false;
let $storage_manager;
let $quota_exceeded_window;
let ignoring_quota_exceeded = false;
async function storage_quota_exceeded() {
if ($quota_exceeded_window) {
$quota_exceeded_window.close();
$quota_exceeded_window = null;
}
if (ignoring_quota_exceeded) {
return;
}
const { promise, $window } = showMessageBox({
title: "Storage Error",
messageHTML: `
async function storage_quota_exceeded() {
if ($quota_exceeded_window) {
$quota_exceeded_window.close();
$quota_exceeded_window = null;
}
if (ignoring_quota_exceeded) {
return;
}
const { promise, $window } = showMessageBox({
title: "Storage Error",
messageHTML: `
<p>JS Paint stores images as you work on them so that if you close your browser or tab or reload the page your images are usually safe.</p>
<p>However, it has run out of space to do so.</p>
<p>You can still save the current image with <b>File > Save</b>. You should save frequently, or free up enough space to keep the image safe.</p>
`,
buttons: [
{ label: "Manage Storage", value: "manage", default: true },
{ label: "Ignore", value: "ignore" },
],
iconID: "warning",
});
$quota_exceeded_window = $window;
const result = await promise;
if (result === "ignore") {
ignoring_quota_exceeded = true;
} else if (result === "manage") {
ignoring_quota_exceeded = false;
manage_storage();
}
}
function manage_storage() {
if ($storage_manager) {
$storage_manager.close();
}
$storage_manager = $DialogWindow().title("Manage Storage").addClass("storage-manager squish");
// @TODO: way to remove all (with confirmation)
const $table = $(E("table")).appendTo($storage_manager.$main);
const $message = $(E("p")).appendTo($storage_manager.$main).html(
"Any images you've saved to your computer with <b>File > Save</b> will not be affected."
);
$storage_manager.$Button("Close", () => {
$storage_manager.close();
});
const addRow = (k, imgSrc) => {
const $tr = $(E("tr")).appendTo($table);
const $img = $(E("img")).attr({ src: imgSrc }).addClass("thumbnail-img");
const $remove = $(E("button")).text("Remove").addClass("remove-button");
const href = `#${k.replace("image#", "local:")}`;
const $open_link = $(E("a")).attr({ href, target: "_blank" }).text(localize("Open"));
const $thumbnail_open_link = $(E("a")).attr({ href, target: "_blank" }).addClass("thumbnail-container");
$thumbnail_open_link.append($img);
$(E("td")).append($thumbnail_open_link).appendTo($tr);
$(E("td")).append($open_link).appendTo($tr);
$(E("td")).append($remove).appendTo($tr);
$remove.on("click", () => {
localStorage.removeItem(k);
$tr.remove();
if ($table.find("tr").length == 0) {
$message.html("<p>All clear!</p>");
}
buttons: [
{ label: "Manage Storage", value: "manage", default: true },
{ label: "Ignore", value: "ignore" },
],
iconID: "warning",
});
};
let localStorageAvailable = false;
try {
if (localStorage.length > 0) {
// This is needed in case it's COMPLETELY full.
// Test with https://stackoverflow.com/questions/45760110/how-to-fill-javascript-localstorage-to-its-max-capacity-quickly
// Of course, this dialog only manages images, not other data (for now anyway).
localStorageAvailable = true;
} else {
localStorage._available = true;
localStorageAvailable = localStorage._available;
delete localStorage._available;
$quota_exceeded_window = $window;
const result = await promise;
if (result === "ignore") {
ignoring_quota_exceeded = true;
} else if (result === "manage") {
ignoring_quota_exceeded = false;
manage_storage();
}
// eslint-disable-next-line no-empty
} catch (e) { }
}
if (localStorageAvailable) {
for (const k in localStorage) {
if (k.match(/^image#/)) {
let v = localStorage[k];
try {
if (v[0] === '"') {
v = JSON.parse(v);
}
// eslint-disable-next-line no-empty
} catch (e) { }
addRow(k, v);
function manage_storage() {
if ($storage_manager) {
$storage_manager.close();
}
$storage_manager = $DialogWindow().title("Manage Storage").addClass("storage-manager squish");
// @TODO: way to remove all (with confirmation)
const $table = $(E("table")).appendTo($storage_manager.$main);
const $message = $(E("p")).appendTo($storage_manager.$main).html(
"Any images you've saved to your computer with <b>File > Save</b> will not be affected."
);
$storage_manager.$Button("Close", () => {
$storage_manager.close();
});
const addRow = (k, imgSrc) => {
const $tr = $(E("tr")).appendTo($table);
const $img = $(E("img")).attr({ src: imgSrc }).addClass("thumbnail-img");
const $remove = $(E("button")).text("Remove").addClass("remove-button");
const href = `#${k.replace("image#", "local:")}`;
const $open_link = $(E("a")).attr({ href, target: "_blank" }).text(localize("Open"));
const $thumbnail_open_link = $(E("a")).attr({ href, target: "_blank" }).addClass("thumbnail-container");
$thumbnail_open_link.append($img);
$(E("td")).append($thumbnail_open_link).appendTo($tr);
$(E("td")).append($open_link).appendTo($tr);
$(E("td")).append($remove).appendTo($tr);
$remove.on("click", () => {
localStorage.removeItem(k);
$tr.remove();
if ($table.find("tr").length == 0) {
$message.html("<p>All clear!</p>");
}
});
};
let localStorageAvailable = false;
try {
if (localStorage.length > 0) {
// This is needed in case it's COMPLETELY full.
// Test with https://stackoverflow.com/questions/45760110/how-to-fill-javascript-localstorage-to-its-max-capacity-quickly
// Of course, this dialog only manages images, not other data (for now anyway).
localStorageAvailable = true;
} else {
localStorage._available = true;
localStorageAvailable = localStorage._available;
delete localStorage._available;
}
// eslint-disable-next-line no-empty
} catch (e) { }
if (localStorageAvailable) {
for (const k in localStorage) {
if (k.match(/^image#/)) {
let v = localStorage[k];
try {
if (v[0] === '"') {
v = JSON.parse(v);
}
// eslint-disable-next-line no-empty
} catch (e) { }
addRow(k, v);
}
}
}
if (!localStorageAvailable) {
// @TODO: DRY with similar message
// @TODO: instructions for your browser; it's called Cookies in chrome/chromium at least, and "storage" gives NO results
$message.html("<p>Please enable local storage in your browser's settings for local backup. It may be called Cookies, Storage, or Site Data.</p>");
} else if ($table.find("tr").length == 0) {
$message.html("<p>All clear!</p>");
}
$storage_manager.$content.width(450);
$storage_manager.center();
$storage_manager.find(".remove-button").focus();
}
if (!localStorageAvailable) {
// @TODO: DRY with similar message
// @TODO: instructions for your browser; it's called Cookies in chrome/chromium at least, and "storage" gives NO results
$message.html("<p>Please enable local storage in your browser's settings for local backup. It may be called Cookies, Storage, or Site Data.</p>");
} else if ($table.find("tr").length == 0) {
$message.html("<p>All clear!</p>");
}
exports.storage_quota_exceeded = storage_quota_exceeded;
exports.manage_storage = manage_storage;
$storage_manager.$content.width(450);
$storage_manager.center();
$storage_manager.find(".remove-button").focus();
}
})(window);

View File

@ -1,129 +1,133 @@
((exports) => {
// Note that this API must be kept in sync with the version in 98.js.org.
// Prefer a function injected from outside an iframe,
// which will make dialogs that can go outside the iframe,
// for 98.js.org integration.
// Note that this API must be kept in sync with the version in 98.js.org.
try {
// <audio> element is simpler for sound effects,
// but in iOS/iPad it shows up in the Control Center, as if it's music you'd want to play/pause/etc.
// It's very silly. Also, on subsequent plays, it only plays part of the sound.
// And Web Audio API is better for playing SFX anyway because it can play a sound overlapping with itself.
window.audioContext = window.audioContext || new AudioContext();
const audio_buffer_promise =
fetch("audio/chord.wav")
.then(response => response.arrayBuffer())
.then(array_buffer => audioContext.decodeAudioData(array_buffer))
var play_chord = async function () {
audioContext.resume(); // in case it was not allowed to start until a user interaction
// Note that this should be before waiting for the audio buffer,
// so that it works the first time.
// (This only works if the message box is opened during a user gesture.)
const audio_buffer = await audio_buffer_promise;
const source = audioContext.createBufferSource();
source.buffer = audio_buffer;
source.connect(audioContext.destination);
source.start();
};
} catch (error) {
console.log("AudioContext not supported", error);
}
function showMessageBox({
title = window.defaultMessageBoxTitle ?? "Alert",
message,
messageHTML,
buttons = [{ label: "OK", value: "ok", default: true }],
iconID = "warning", // "error", "warning", "info", or "nuke" for deleting files/folders
windowOptions = {}, // for controlling width, etc.
}) {
let $window, $message;
const promise = new Promise((resolve, reject) => {
$window = make_window_supporting_scale(Object.assign({
title,
resizable: false,
innerWidth: 400,
maximizeButton: false,
minimizeButton: false,
}, windowOptions));
// $window.addClass("dialog-window horizontal-buttons");
$message =
$("<div>").css({
textAlign: "left",
fontFamily: "MS Sans Serif, Arial, sans-serif",
fontSize: "14px",
marginTop: "22px",
flex: 1,
minWidth: 0, // Fixes hidden overflow, see https://css-tricks.com/flexbox-truncated-text/
whiteSpace: "normal", // overriding .window:not(.squish)
});
if (messageHTML) {
$message.html(messageHTML);
} else if (message) { // both are optional because you may populate later with dynamic content
$message.text(message).css({
whiteSpace: "pre-wrap",
wordWrap: "break-word",
});
}
$("<div>").append(
$("<img width='32' height='32'>").attr("src", `images/${iconID}-32x32-8bpp.png`).css({
margin: "16px",
display: "block",
}),
$message
).css({
display: "flex",
flexDirection: "row",
}).appendTo($window.$content);
$window.$content.css({
textAlign: "center",
});
for (const button of buttons) {
const $button = $window.$Button(button.label, () => {
button.action?.(); // API may be required for using user gesture requiring APIs
resolve(button.value);
$window.close(); // actually happens automatically
});
if (button.default) {
$button.addClass("default");
$button.focus();
setTimeout(() => $button.focus(), 0); // @TODO: why is this needed? does it have to do with the iframe window handling?
}
$button.css({
minWidth: 75,
height: 23,
margin: "16px 2px",
});
}
$window.on("focusin", "button", (event) => {
$(event.currentTarget).addClass("default");
});
$window.on("focusout", "button", (event) => {
$(event.currentTarget).removeClass("default");
});
$window.on("closed", () => {
resolve("closed"); // or "cancel"? do you need to distinguish?
});
$window.center();
});
promise.$window = $window;
promise.$message = $message;
promise.promise = promise; // for easy destructuring
try {
play_chord();
} catch (error) {
console.log(`Failed to play ${chord_audio.src}: `, error);
}
return promise;
}
// Prefer a function injected from outside an iframe,
// which will make dialogs that can go outside the iframe,
// for 98.js.org integration.
// exports.showMessageBox = window.showMessageBox;
exports.showMessageBox = exports.showMessageBox || showMessageBox;
})(window);
// Note `defaultMessageBoxTitle` handling in make_iframe_window
// Any other default parameters need to be handled there (as it works now)
window.defaultMessageBoxTitle = localize("Paint");
try {
// <audio> element is simpler for sound effects,
// but in iOS/iPad it shows up in the Control Center, as if it's music you'd want to play/pause/etc.
// It's very silly. Also, on subsequent plays, it only plays part of the sound.
// And Web Audio API is better for playing SFX anyway because it can play a sound overlapping with itself.
window.audioContext = window.audioContext || new AudioContext();
const audio_buffer_promise =
fetch("audio/chord.wav")
.then(response => response.arrayBuffer())
.then(array_buffer => audioContext.decodeAudioData(array_buffer))
var play_chord = async function () {
audioContext.resume(); // in case it was not allowed to start until a user interaction
// Note that this should be before waiting for the audio buffer,
// so that it works the first time.
// (This only works if the message box is opened during a user gesture.)
const audio_buffer = await audio_buffer_promise;
const source = audioContext.createBufferSource();
source.buffer = audio_buffer;
source.connect(audioContext.destination);
source.start();
};
} catch (error) {
console.log("AudioContext not supported", error);
}
window.showMessageBox = window.showMessageBox || (({
title = window.defaultMessageBoxTitle ?? "Alert",
message,
messageHTML,
buttons = [{ label: "OK", value: "ok", default: true }],
iconID = "warning", // "error", "warning", "info", or "nuke" for deleting files/folders
windowOptions = {}, // for controlling width, etc.
}) => {
let $window, $message;
const promise = new Promise((resolve, reject) => {
$window = make_window_supporting_scale(Object.assign({
title,
resizable: false,
innerWidth: 400,
maximizeButton: false,
minimizeButton: false,
}, windowOptions));
// $window.addClass("dialog-window horizontal-buttons");
$message =
$("<div>").css({
textAlign: "left",
fontFamily: "MS Sans Serif, Arial, sans-serif",
fontSize: "14px",
marginTop: "22px",
flex: 1,
minWidth: 0, // Fixes hidden overflow, see https://css-tricks.com/flexbox-truncated-text/
whiteSpace: "normal", // overriding .window:not(.squish)
});
if (messageHTML) {
$message.html(messageHTML);
} else if (message) { // both are optional because you may populate later with dynamic content
$message.text(message).css({
whiteSpace: "pre-wrap",
wordWrap: "break-word",
});
}
$("<div>").append(
$("<img width='32' height='32'>").attr("src", `images/${iconID}-32x32-8bpp.png`).css({
margin: "16px",
display: "block",
}),
$message
).css({
display: "flex",
flexDirection: "row",
}).appendTo($window.$content);
$window.$content.css({
textAlign: "center",
});
for (const button of buttons) {
const $button = $window.$Button(button.label, () => {
button.action?.(); // API may be required for using user gesture requiring APIs
resolve(button.value);
$window.close(); // actually happens automatically
});
if (button.default) {
$button.addClass("default");
$button.focus();
setTimeout(() => $button.focus(), 0); // @TODO: why is this needed? does it have to do with the iframe window handling?
}
$button.css({
minWidth: 75,
height: 23,
margin: "16px 2px",
});
}
$window.on("focusin", "button", (event) => {
$(event.currentTarget).addClass("default");
});
$window.on("focusout", "button", (event) => {
$(event.currentTarget).removeClass("default");
});
$window.on("closed", () => {
resolve("closed"); // or "cancel"? do you need to distinguish?
});
$window.center();
});
promise.$window = $window;
promise.$message = $message;
promise.promise = promise; // for easy destructuring
try {
play_chord();
} catch (error) {
console.log(`Failed to play ${chord_audio.src}: `, error);
}
return promise;
});
// Don't override alert, because I only use it as a fallback for global error handling.
// If make_window_supporting_scale is not defined, then alert is used instead,
// so it must not also end up calling make_window_supporting_scale.