Wrap many modules in IIFEs to track their exports
parent
4fdd061507
commit
355fba3ee2
270
src/$ColorBox.js
270
src/$ColorBox.js
|
@ -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);
|
|
@ -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);
|
||||
|
|
171
src/$FontBox.js
171
src/$FontBox.js
|
@ -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);
|
226
src/$ToolBox.js
226
src/$ToolBox.js
|
@ -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);
|
|
@ -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
1208
src/edit-colors.js
1208
src/edit-colors.js
File diff suppressed because it is too large
Load Diff
908
src/help.js
908
src/help.js
|
@ -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);
|
489
src/helpers.js
489
src/helpers.js
|
@ -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);
|
368
src/imgur.js
368
src/imgur.js
|
@ -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);
|
|
@ -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);
|
242
src/msgbox.js
242
src/msgbox.js
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue