Update os-gui to 0.3.0, unifying windowing code

Enable resizable windows! And proper dialog window styles!
Tool window titlebar text is truncated with ellipses now instead of making the window ridiculously wide for some languages, e.g. Español which reads "Herramientas" for "Tools"
main
Isaiah Odhner 2021-09-02 18:19:51 -04:00
parent 3ffd28ae6d
commit 847fbc394b
28 changed files with 767 additions and 707 deletions

View File

@ -114,7 +114,7 @@ module.exports = {
"$ColorBox": "writable",
"$Window": "writable",
"$ToolWindow": "writable",
"$FormToolWindow": "writable",
"$DialogWindow": "writable",
"$Handles": "writable",
"$ChooseShapeStyle": "writable",
"OnCanvasSelection": "writable",

402
.vscode/settings.json vendored
View File

@ -9,25 +9,35 @@
"**/out": true
},
"cSpell.words": [
"APNGs",
"Adlm",
"Æвзаг",
"Afaan",
"Afaraf",
"airb",
"ajeļ",
"Allaire",
"allowfullscreen",
"anypalette",
"apng",
"APNGs",
"Aragonés",
"Asụsụ",
"Avaric",
"autosave",
"autoupdating",
"Avañe'ẽ",
"Avaric",
"Ayisyen",
"Aymar",
"Azərbaycan",
"BMPs",
"Bahasa",
"Bamanankan",
"Basa",
"bepis",
"Bgau",
"bgcolor",
"Bislama",
"Bizaad",
"BMPs",
"Bokmål",
"Bopo",
"Bosanski",
@ -36,88 +46,150 @@
"Bresenham's",
"Brezhoneg",
"Català",
"Český",
"Čeština",
"Chamoru",
"Chewa",
"Chichewa",
"Chinyanja",
"Chuang",
"classid",
"clipart",
"Clippy",
"clsid",
"cmaps",
"colorbox",
"contenteditable",
"Corel",
"Corsa",
"Corsu",
"Cpath",
"Crect",
"Csvg",
"ctype",
"Cue",
"Cuengh",
"Cueŋƅ",
"Cymraeg",
"Cyrl",
"DIALOGEX",
"d'Òc",
"Dansk",
"Darude",
"Davvis",
"Davvisámegiella",
"desaturated",
"Deutsch",
"Dhivehi",
"DIALOGEX",
"Dili",
"Divehi",
"djvu",
"Dorerin",
"draggable",
"Dzongkha",
"Eesti",
"egbe",
"ellipticals",
"endonym",
"eqeqeq",
"equivalize",
"ertical",
"esque",
"Euskara",
"Euskera",
"Excelsi",
"Eʋegbe",
"Excelsi",
"eyedrop",
"Fa'a",
"Faka",
"farbling",
"Faroese",
"fieldsets",
"firebaseapp",
"Fiteny",
"fliph",
"flippable",
"flipv",
"floodfill",
"floodfilling",
"focusables",
"focusring",
"fontbox",
"Føroyskt",
"Fran",
"Français",
"Française",
"frowny",
"Frysk",
"Fsvg",
"fudgedness",
"Fulah",
"Fulfulde",
"Fwww",
"Føroyskt",
"GIFs",
"Gaeilge",
"Gaelg",
"Gagana",
"Gàidhlig",
"Gailck",
"Galego",
"gazemouse",
"ghostwhite",
"GIFs",
"Gikuyu",
"Glag",
"gons",
"gridlines",
"Grischun",
"Gàidhlig",
"HKEY",
"hackily",
"hacky",
"haha",
"Hanb",
"Hant",
"hcanvas",
"hctx",
"headmouse",
"hilight",
"Hiri",
"HKEY",
"hostnames",
"Hrvatski",
"hslrgb",
"hsrgb",
"humbnail",
"icns",
"iconify",
"idhlig",
"IFDs",
"iframe's",
"iframes",
"IIFE",
"Ikinyarwanda",
"Ikirundi",
"Imgur",
"Inkscape",
"Interlingue",
"Italiano",
"Iñupiatun",
"JSGF",
"isaiahodhner",
"isded",
"Íslenska",
"Italiano",
"Jasc",
"Jawa",
"Jazyk",
"Jezik",
"Język",
"jfif",
"jnordberg",
"JSGF",
"jspaint",
"jsperf",
"Kajin",
"Kalaallisut",
"Kalaallit",
"Kalba",
"Kanuri",
"Kernewek",
"keyshortcuts",
"Khoj",
"Kichwa",
"Kieli",
@ -137,185 +209,17 @@
"Latvie",
"Latviešu",
"Lenga",
"lerp",
"Lëtzebuergesch",
"Letzeburgesch",
"libgconf",
"libtess",
"Lietuvių",
"Limburgan",
"Limburgish",
"Limburgs",
"Linb",
"Lingála",
"Luba",
"Luciferi",
"Luxembourgish",
"Lëtzebuergesch",
"MSIE",
"Macromedia",
"Malti",
"Melayu",
"Mopaint",
"Motu",
"Naoero",
"Nbat",
"Ndebele",
"Ndonga",
"Nederlands",
"Nkoo",
"Norsk",
"Nuosu",
"Nuosuhxop",
"Nyanja",
"Occitan",
"Odhner",
"Optikey",
"Oqaasii",
"Oromo",
"Oromoo",
"Ossetian",
"Otjiherero",
"Owambo",
"Oʻzbek",
"PDFs",
"PLTE",
"PNGs",
"Phlp",
"Photoshop",
"Polski",
"Polszczyzna",
"Português",
"Pulaar",
"Pular",
"Pushto",
"RGBAs",
"RTLCSS",
"Rege",
"Română",
"Rumantsch",
"Runa",
"Rundi",
"STRINGTABLE",
"Sami",
"Sango",
"Sardu",
"Satana",
"Satanas",
"Scribus",
"Sesotho",
"Setswana",
"Shft",
"Shona",
"Shqip",
"Simi",
"Sinhala",
"Skencil",
"Slovenski",
"Slovenský",
"Slovenčina",
"Slovenščina",
"Soomaali",
"Soomaaliga",
"Sotho",
"Suomen",
"Svenska",
"Swati",
"Tiếng",
"Toçikī",
"Tshivenḓa",
"Tsonga",
"Tvcy",
"Türkçe",
"UPNG",
"UTIF",
"Uyghur",
"Uyghurche",
"VSNs",
"Vakaviti",
"Valencian",
"Valoda",
"Venda",
"Viacam",
"Việt",
"Vlaams",
"Volapük",
"Vosa",
"Walon",
"Wayback",
"Wikang",
"Wollof",
"Xitsonga",
"Yângâ",
"Zhuang",
"Zhōngwén",
"airb",
"ajeļ",
"allowfullscreen",
"anypalette",
"apng",
"autosave",
"autoupdating",
"bepis",
"bgcolor",
"classid",
"clipart",
"clsid",
"cmaps",
"colorbox",
"contenteditable",
"ctype",
"d'Òc",
"desaturated",
"djvu",
"draggable",
"egbe",
"ellipticals",
"endonym",
"eqeqeq",
"equivalize",
"ertical",
"esque",
"eyedrop",
"farbling",
"fieldsets",
"firebaseapp",
"fliph",
"flippable",
"flipv",
"floodfill",
"floodfilling",
"focusables",
"focusring",
"fontbox",
"frowny",
"fudgedness",
"gazemouse",
"ghostwhite",
"gons",
"gridlines",
"hackily",
"hacky",
"haha",
"hcanvas",
"hctx",
"headmouse",
"hilight",
"hostnames",
"hslrgb",
"hsrgb",
"humbnail",
"icns",
"iconify",
"idhlig",
"iframe's",
"iframes",
"isaiahodhner",
"isded",
"jfif",
"jnordberg",
"jspaint",
"jsperf",
"keyshortcuts",
"lerp",
"libgconf",
"libtess",
"liveweb",
"llpaper",
"localdomain",
@ -323,49 +227,90 @@
"lrgb",
"lsid",
"ltres",
"Luba",
"Luciferi",
"Luxembourgish",
"Macromedia",
"Malti",
"mediump",
"megiella",
"Melayu",
"mimg",
"mobipaint",
"monospace",
"Mopaint",
"Motu",
"mouseleave",
"msapplication",
"MSIE",
"mspaint",
"multitools",
"multitouch",
"multiuser",
"murl",
"Naoero",
"Nbat",
"Ndebele",
"Ndonga",
"Nederlands",
"nesw",
"nevermind",
"Nkoo",
"nomine",
"Norsk",
"nostri",
"numberofcolors",
"numpad",
"Nuosu",
"Nuosuhxop",
"nwse",
"Nyanja",
"Occitan",
"occluder",
"octree",
"Odhner",
"Oʻzbek",
"oleobject",
"onwriteend",
"Optikey",
"Oqaasii",
"orizontal",
"Oromo",
"Oromoo",
"Ossetian",
"Otjiherero",
"ovaloid",
"ovaloids",
"oviforms",
"Owambo",
"paintbucket",
"pako",
"palettized",
"paypal",
"PDFs",
"peggys",
"Phlp",
"Photoshop",
"pixeling",
"PLTE",
"PNGs",
"pointerenter",
"pointerleave",
"pointermove",
"pointerup",
"Polski",
"Polszczyzna",
"Português",
"pseudorandomly",
"psppalette",
"Pulaar",
"Pular",
"Pushto",
"qtres",
"rbaycan",
"redoable",
"reenable",
"Rege",
"reimplement",
"repurposable",
"rerender",
@ -374,29 +319,61 @@
"retarget",
"retargeted",
"rgba",
"RGBAs",
"rightclick",
"rk",
"Română",
"rotologo",
"roundrect",
"roundrects",
"royskt",
"rrect",
"RTLCSS",
"Rumantsch",
"Runa",
"Rundi",
"Sami",
"Sango",
"Sardu",
"Satana",
"Satanas",
"Scribus",
"scrollable",
"Sesotho",
"Setswana",
"shader's",
"Shft",
"Shona",
"Shqip",
"Simi",
"Sinhala",
"Skencil",
"sketchpalette",
"skeuomorphic",
"slenska",
"Slovenčina",
"Slovenščina",
"Slovenski",
"Slovenský",
"Soomaali",
"Soomaaliga",
"sorthweast",
"Sotho",
"soundcloud",
"spacebar",
"spraycan",
"spraypaint",
"spraypainting",
"STRINGTABLE",
"styl",
"stylable",
"submenu",
"submenus",
"subrepo",
"subwindows",
"Suomen",
"Svenska",
"Swati",
"tabbable",
"tabindex",
"tesselator",
@ -406,13 +383,19 @@
"textboxes",
"themepack",
"throwie",
"Tiếng",
"tileable",
"timespan",
"tina",
"titlebar",
"Toçikī",
"togglable",
"tracedata",
"tracky",
"Tshivenḓa",
"Tsonga",
"Türkçe",
"Tvcy",
"tzebuergesch",
"ufeff",
"undecagons",
@ -423,31 +406,48 @@
"unfocusing",
"uniquify",
"unmaximize",
"unminimize",
"unpremultiplied",
"untrusted",
"upiatun",
"UPNG",
"ustom",
"UTIF",
"Uyghur",
"Uyghurche",
"Vakaviti",
"Valencian",
"Valoda",
"vaporwave",
"Venda",
"verts",
"Viacam",
"Việt",
"viewports",
"Vlaams",
"Volapük",
"Vosa",
"VSNs",
"Walon",
"Wayback",
"webglcontextlost",
"webglcontextrestored",
"webp",
"Wikang",
"",
"woah",
"Wollof",
"Xitsonga",
"xtras",
"Yângâ",
"youtube",
"zbek",
"Zhōngwén",
"Zhuang",
"zoomable",
"zoomer",
"zyk",
"Æвзаг",
"Íslenska",
"Český",
"Čeština",
"Ελληνικά",
"Јазик",
"Језик",
"Ўзбек",
"Авар",
"Аҧсуа",
"Аҧсшәа",
@ -457,9 +457,12 @@
"Бызшәа",
"Език",
"Ирон",
"Јазик",
"Језик",
"Коми",
"Кыргыз",
"Кыргызча",
"Қазақ",
"Македонски",
"Мова",
"Монгол",
@ -471,15 +474,16 @@
"Татар",
"Теле",
"Тили",
"Тоҷикӣ",
"Тілі",
"Тоҷикӣ",
"Түркмен",
"Ўзбек",
"Українська",
"Чӑваш",
"Чӗлхи",
"Ѩзыкъ",
"Қазақ",
"Ӏарул",
"ქართული",
"Հայերեն",
"עברית",
"أۇزبېك",
@ -487,23 +491,21 @@
"اردو",
"العربية",
"بهاس",
"پښتو",
"پنجابی",
"تاجیکی",
"سندھی",
"سنڌي",
"فارسی",
"كشميري",
"ملايو",
"پنجابی",
"پښتو",
"کوردی",
"ພາສາລາວ",
"ქართული",
"ملايو",
"ትግርኛ",
"አማርኛ",
"ພາສາລາວ",
"ꦧꦱꦗꦮ",
"ᐃᓄᒃᑎᑐᑦ",
"ᐊᓂᔑᓈᐯᒧᐎᓐ",
"ᓀᐦᐃᔭᐍᐏᐣ",
"ꦧꦱꦗꦮ",
""
"ᓀᐦᐃᔭᐍᐏᐣ"
]
}

View File

@ -31,8 +31,9 @@ See [**Development Setup**](./README.md#Development-Setup) on the readme.
- `functions.js` has functions that shouldn't own any global state, altho they very much modify global state, and there may be a few stateful global variables defined in there.
- The project uses [jQuery](https://jquery.com/), and a convention of prefixing variables that hold jQuery objects with `$`
- There are also some weird pseudo-classes like `$ColorBox` which extend and return jQuery objects. I don't recommend this pattern for new code.
- Menu code and some windowing code is in `lib/os-gui/` and should be kept in sync with the [os-gui](https://github.com/1j01/os-gui) project.
- Some other windowing code is in $ToolWindow.js, for windows that don't have maximize/minimize buttons; eventually this should be provided by os-gui.
- Menu code and windowing code is in `lib/os-gui/` and should be kept in sync with the [os-gui](https://github.com/1j01/os-gui) project.
- (Maybe I should version this using git-subrepo?)
- Some window behavior specific to jspaint is in `$ToolWindow.js` and `$Component.js`
- `image-manipulation.js` should contain just rendering related code, and ideally no dialogs except maybe some error messages.
- Some image manipulation code is also in `tools.js` and `functions.js`
- CSS is in `styles/`

View File

@ -188,7 +188,7 @@ Functionality:
* JS
* Organize things into files better; "functions.js" is like ONE step above saying "code.js"
* `$ToolWindow` has a `$Button` facility; `$FormToolWindow` overrides it with essentially a better one
* `$ToolWindow` has a `$Button` facility; `$DialogWindow` overrides it with essentially a better one
* Make code clearer / improve code quality
* https://codeclimate.com/github/1j01/jspaint

View File

@ -402,7 +402,7 @@
<script src="src/$Component.js"></script>
<script src="src/$ToolWindow.js"></script>
<script>
// after show_error_message, $FormToolWindow, and localize are defined,
// after show_error_message, $DialogWindow, and localize are defined,
// set up better global error handling
const old_onerror = window.onerror;
window.onerror = (message, source, lineno, colno, error)=> {

View File

@ -1,4 +1,4 @@
(function(exports) {
((exports)=> {
// TODO: E\("([a-z]+)"\) -> "<$1>" or get rid of jQuery as a dependency
function E(t){
@ -229,7 +229,7 @@ function $MenuBar(menus){
}
if(String.fromCharCode(e.keyCode) === get_hotkey(item.item)){
e.preventDefault();
$item.trogger("click");
$item.trigger("click");
}
});
}

View File

@ -1,7 +1,7 @@
(function(exports) {
((exports) => {
// TODO: E\("([a-z]+)"\) -> "<$1>" or get rid of jQuery as a dependency
function E(t){
function E(t) {
return document.createElement(t);
}
@ -10,69 +10,98 @@ var $G = $(window);
$Window.Z_INDEX = 5;
function $Window(options){
function $Window(options) {
options = options || {};
var $w = $(E("div")).addClass("window os-window").appendTo("body");
$w.$titlebar = $(E("div")).addClass("window-titlebar").appendTo($w);
$w.$title_area = $(E("div")).addClass("window-title-area").appendTo($w.$titlebar);
$w.$title = $(E("span")).addClass("window-title").appendTo($w.$title_area);
$w.$minimize = $(E("button")).addClass("window-minimize-button window-button").appendTo($w.$titlebar);
$w.$maximize = $(E("button")).addClass("window-maximize-button window-button").appendTo($w.$titlebar);
$w.$x = $(E("button")).addClass("window-close-button window-button").appendTo($w.$titlebar);
if (options.toolWindow) {
options.minimizeButton = false;
options.maximizeButton = false;
}
if (options.minimizeButton !== false) {
$w.$minimize = $(E("button")).addClass("window-minimize-button window-button").appendTo($w.$titlebar);
}
if (options.maximizeButton !== false) {
$w.$maximize = $(E("button")).addClass("window-maximize-button window-button").appendTo($w.$titlebar);
}
if (options.closeButton !== false) {
$w.$x = $(E("button")).addClass("window-close-button window-button").appendTo($w.$titlebar);
}
$w.$content = $(E("div")).addClass("window-content").appendTo($w);
if (options.toolWindow) {
$w.addClass("tool-window");
}
if (options.parentWindow) {
options.parentWindow.addChildWindow($w);
}
var $component = options.$component;
if(options.icon){
if (options.icon) {
$w.icon_name = options.icon;
$w.$icon = $Icon(options.icon, TITLEBAR_ICON_SIZE).prependTo($w.$titlebar);
}
if($component){
if ($component) {
$w.addClass("component-window");
}
const $eventTarget = $({});
const makeSimpleListenable = (name)=> {
return (callback)=> {
const fn = ()=> {
const $event_target = $({});
const make_simple_listenable = (name) => {
return (callback) => {
const fn = () => {
callback();
};
$eventTarget.on(name, fn);
const dispose = ()=> {
$eventTarget.off(name, fn);
$event_target.on(name, fn);
const dispose = () => {
$event_target.off(name, fn);
};
return dispose;
};
};
$w.onFocus = makeSimpleListenable("focus");
$w.onBlur = makeSimpleListenable("blur");
$w.onClosed = makeSimpleListenable("closed");
$w.onFocus = make_simple_listenable("focus");
$w.onBlur = make_simple_listenable("blur");
$w.onClosed = make_simple_listenable("closed");
$w.focus = ()=> {
let child$Windows = [];
let $focusShowers = $w;
$w.addChildWindow = ($childWindow) => {
child$Windows.push($childWindow);
$focusShowers = $focusShowers.add($childWindow);
};
$w.focus = () => {
if (options.parentWindow) {
// TODO: remove flicker of unfocused state (for both child and parent windows)
setTimeout((() => { // wait til after blur handler of parent window
options.parentWindow.focus();
}), 0);
return;
}
if (window.focusedWindow === $w) {
return;
}
window.focusedWindow && focusedWindow.blur();
$w.bringToFront();
$w.addClass("focused");
$focusShowers.addClass("focused");
window.focusedWindow = $w;
$eventTarget.triggerHandler("focus");
$event_target.triggerHandler("focus");
};
$w.blur = ()=> {
$w.blur = () => {
if (window.focusedWindow !== $w) {
return;
}
$w.removeClass("focused");
$focusShowers.removeClass("focused");
// TODO: document.activeElement && document.activeElement.blur()?
$eventTarget.triggerHandler("blur");
$event_target.triggerHandler("blur");
window.focusedWindow = null;
};
$w.on("focusin pointerdown", function(e){
$w.on("focusin pointerdown", (e) => {
$w.focus();
});
$G.on("pointerdown", (e)=> {
$G.on("pointerdown", (e) => {
if (
e.target.closest(".os-window") !== $w[0] &&
!e.target.closest(".taskbar")
@ -81,31 +110,31 @@ function $Window(options){
}
});
$w.attr("touch-action", "none");
$w.$x.on("click", function(){
$w.css("touch-action", "none");
$w.$x?.on("click", () => {
$w.close();
});
$w.minimize = function() {
$w.minimize = () => {
if ($w.is(":visible")) {
const $task = this.task.$task;
const $task = $w.task.$task;
const before_rect = $w.$titlebar[0].getBoundingClientRect();
const after_rect = $task[0].getBoundingClientRect();
$w.animateTitlebar(before_rect, after_rect, ()=> {
$w.animateTitlebar(before_rect, after_rect, () => {
$w.hide();
$w.blur();
});
}
};
$w.unminimize = function() {
$w.unminimize = () => {
if ($w.is(":hidden")) {
const $task = this.task.$task;
const $task = $w.task.$task;
const before_rect = $task[0].getBoundingClientRect();
$w.show();
const after_rect = $w.$titlebar[0].getBoundingClientRect();
$w.hide();
$w.animateTitlebar(before_rect, after_rect, ()=> {
$w.animateTitlebar(before_rect, after_rect, () => {
$w.show();
$w.bringToFront();
$w.focus();
@ -114,9 +143,9 @@ function $Window(options){
};
let before_maximize;
$w.$maximize.on("click", function(){
$w.$maximize?.on("click", () => {
const instantly_maximize = ()=> {
const instantly_maximize = () => {
before_maximize = {
position: $w.css("position"),
left: $w.css("left"),
@ -124,7 +153,7 @@ function $Window(options){
width: $w.css("width"),
height: $w.css("height"),
};
$w.addClass("maximized");
const $taskbar = $(".taskbar");
const scrollbar_width = window.innerWidth - $(window).width();
@ -138,9 +167,9 @@ function $Window(options){
height: `calc(100vh - ${scrollbar_height}px - ${taskbar_height}px)`,
});
};
const instantly_unmaximize = ()=> {
const instantly_unmaximize = () => {
$w.removeClass("maximized");
$w.css({width: "", height: ""});
$w.css({ width: "", height: "" });
if (before_maximize) {
$w.css({
position: before_maximize.position,
@ -164,7 +193,7 @@ function $Window(options){
after_rect = $w.$titlebar[0].getBoundingClientRect();
instantly_unmaximize();
}
$w.animateTitlebar(before_rect, after_rect, ()=> {
$w.animateTitlebar(before_rect, after_rect, () => {
if ($w.hasClass("maximized")) {
instantly_unmaximize();
} else {
@ -172,67 +201,117 @@ function $Window(options){
}
});
});
$w.$minimize.on("click", function(){
$w.$minimize?.on("click", () => {
$w.minimize();
});
$w.$title_area.on("mousedown selectstart", ".window-button", function(e){
$w.$title_area.on("mousedown selectstart", ".window-button", (e) => {
e.preventDefault();
});
$w.$title_area.on("dblclick", ()=> {
$w.$maximize.triggerHandler("click");
$w.$title_area.on("dblclick", () => {
$w.$maximize?.triggerHandler("click");
});
$w.css({
position: "absolute",
zIndex: $Window.Z_INDEX++
});
$w.bringToFront = function(){
$w.bringToFront = () => {
$w.css({
zIndex: $Window.Z_INDEX++
});
for (const $childWindow of child$Windows) {
$childWindow.bringToFront();
}
};
$w.on("pointerdown", function(){
// var focused = false;
var last_focused_control;
$w.on("pointerdown refocus-window", (event) => {
$w.bringToFront();
// Test cases where it should refocus the last focused control in the window:
// - Click in the blank space of the window
// - Click on the window title bar
// - Close a second window, focusing the first window
// - Clicking on a control in the window should focus it, by way of updating last_focused_control
// - Simulated clicks
// It should NOT refocus when:
// - Clicking on a control in a different window
// - Trying to select text
// Wait for other pointerdown handlers and default behavior, and focusin events.
// Set focus to the last focused control, which should be updated if a click just occurred.
requestAnimationFrame(() => {
// focused = true;
// But if the element is selectable, wait until the click is done and see if anything was selected first.
// This is a bit of a weird compromise, for now.
const target_style = getComputedStyle(event.target);
if (target_style.userSelect !== "none") {
$w.one("pointerup pointercancel", () => {
requestAnimationFrame(() => { // this seems to make it more reliable in regards to double clicking
if (last_focused_control && !getSelection().toString().trim()) {
last_focused_control.focus();
}
});
});
return;
}
if (last_focused_control) {
last_focused_control.focus();
}
});
});
$w.on("keydown", function(e){
if(e.ctrlKey || e.altKey || e.shiftKey){
// Assumption: no control exists in the window before, this "focusin" handler is set up,
// so any element.focus() will be after and trigger this handler.
$w.on("focusin", () => {
// focused = true;
if (document.activeElement && $.contains($w[0], document.activeElement)) {
last_focused_control = document.activeElement;
}
});
// $w.on("focusout", ()=> {
// requestAnimationFrame(()=> {
// if (!document.activeElement || !$.contains($w[0], document.activeElement)) {
// focused = false;
// }
// });
// });
$w.on("keydown", (e) => {
if (e.ctrlKey || e.altKey || e.metaKey) {
return;
}
var $buttons = $w.$content.find("button");
var $focused = $(document.activeElement);
var focused_index = $buttons.index($focused);
// console.log(e.keyCode);
switch(e.keyCode){
const $buttons = $w.$content.find("button");
const $focused = $(document.activeElement);
const focused_index = $buttons.index($focused);
switch (e.keyCode) {
case 40: // Down
case 39: // Right
if($focused.is("button")){
if(focused_index < $buttons.length - 1){
$buttons.get(focused_index + 1).focus();
if ($focused.is("button") && !e.shiftKey) {
if (focused_index < $buttons.length - 1) {
$buttons[focused_index + 1].focus();
e.preventDefault();
}
}
break;
case 38: // Up
case 37: // Left
if($focused.is("button")){
if(focused_index > 0){
$buttons.get(focused_index - 1).focus();
if ($focused.is("button") && !e.shiftKey) {
if (focused_index > 0) {
$buttons[focused_index - 1].focus();
e.preventDefault();
}
}
break;
case 32: // Space
case 13: // Enter (doesn't actually work in chrome because the button gets clicked immediately)
if($focused.is("button")){
if ($focused.is("button") && !e.shiftKey) {
$focused.addClass("pressed");
var release = function(){
const release = () => {
$focused.removeClass("pressed");
$focused.off("focusout", release);
$(window).off("keyup", keyup);
};
var keyup = function(e){
if(e.keyCode === 32 || e.keyCode === 13){
const keyup = (e) => {
if (e.keyCode === 32 || e.keyCode === 13) {
release();
}
};
@ -240,129 +319,321 @@ function $Window(options){
$(window).on("keyup", keyup);
}
break;
case 9: // Tab
case 9: { // Tab
// wrap around when tabbing through controls in a window
var $controls = $w.$content.find("input, textarea, select, button, a");
var focused_control_index = $controls.index($focused);
if(focused_control_index === $controls.length - 1){
e.preventDefault();
$controls[0].focus();
// @#: focusables
let $controls = $w.$content.find("input, textarea, select, button, object, a[href], [tabIndex='0'], details summary").filter(":enabled, summary, a").filter(":visible");
// const $controls = $w.$content.find(":tabbable"); // https://api.jqueryui.com/tabbable-selector/
// Radio buttons should be treated as a group with one tabstop.
// If there's no selected ("checked") radio, it should still visit the group,
// but it should skip all unselected radios in that group if there is a selected radio in that group.
const radios = {}; // best radio found so far, per group
const to_skip = [];
for (const el of $controls) {
if (el.nodeName.toLowerCase() === "input" && el.type === "radio") {
if (radios[el.name]) {
if (el.checked) {
to_skip.push(radios[el.name]);
radios[el.name] = el;
} else {
to_skip.push(el);
}
} else {
radios[el.name] = el;
}
}
}
$controls = $controls.not(to_skip);
// debug viz:
// $controls.css({boxShadow: "0 0 2px 2px green"});
// $(toSkip).css({boxShadow: "0 0 2px 2px gray"})
if ($controls.length > 0) {
const focused_control_index = $controls.index($focused);
if (e.shiftKey) {
if (focused_control_index === 0) {
e.preventDefault();
$controls[$controls.length - 1].focus();
}
} else {
if (focused_control_index === $controls.length - 1) {
e.preventDefault();
$controls[0].focus();
}
}
}
break;
}
case 27: // Esc
$w.close();
break;
}
});
// @TODO: restore last focused controls when clicking/mousing down on the window
$w.applyBounds = () => {
// TODO: outerWidth vs width? not sure
$w.css({
left: Math.max(0, Math.min(innerWidth - $w.outerWidth(), $w[0].getBoundingClientRect().left)),
top: Math.max(0, Math.min(innerHeight - $w.outerHeight(), $w[0].getBoundingClientRect().top)),
left: Math.max(0, Math.min(document.body.scrollWidth - $w.width(), $w.position().left)),
top: Math.max(0, Math.min(document.body.scrollHeight - $w.height(), $w.position().top)),
});
};
$w.bringTitleBarOnScreen = () => {
$w.bringTitleBarInBounds = () => {
// Try to make the titlebar always accessible
const min_horizontal_pixels_on_screen = 40; // enough for space past a close button
$w.css({
left: Math.max(
min_horizontal_pixels_on_screen - $w.outerWidth(),
Math.min(
innerWidth - min_horizontal_pixels_on_screen,
$w[0].getBoundingClientRect().left
document.body.scrollWidth - min_horizontal_pixels_on_screen,
$w.position().left
)
),
top: Math.max(0, Math.min(
innerHeight - $w.$titlebar.outerHeight() - 5,
$w[0].getBoundingClientRect().top
document.body.scrollHeight - $w.$titlebar.outerHeight() - 5,
$w.position().top
)),
});
};
$w.center = function(){
$w.center = () => {
$w.css({
left: (innerWidth - $w.width()) / 2 + window.scrollX,
top: (innerHeight - $w.height()) / 2 + window.scrollY,
});
$w.applyBounds();
};
$G.on("resize", $w.bringTitleBarOnScreen);
var drag_offset_x, drag_offset_y;
var mouse_x, mouse_y;
var update_drag = function(e){
mouse_x = e.clientX != null ? e.clientX : mouse_x;
mouse_y = e.clientY != null ? e.clientY : mouse_y;
$G.on("resize", $w.bringTitleBarInBounds);
var drag_offset_x, drag_offset_y, drag_pointer_x, drag_pointer_y, drag_pointer_id;
var update_drag = (e) => {
if (drag_pointer_id === e.pointerId) {
drag_pointer_x = e.clientX ?? drag_pointer_x;
drag_pointer_y = e.clientY ?? drag_pointer_y;
}
$w.css({
left: mouse_x + scrollX - drag_offset_x,
top: mouse_y + scrollY - drag_offset_y,
left: drag_pointer_x + scrollX - drag_offset_x,
top: drag_pointer_y + scrollY - drag_offset_y,
});
};
$w.$titlebar.attr("touch-action", "none");
$w.$titlebar.on("mousedown selectstart", function(e){
$w.$titlebar.css("touch-action", "none");
$w.$titlebar.on("mousedown selectstart", (e) => {
e.preventDefault();
});
$w.$titlebar.on("pointerdown", function(e){
if($(e.target).closest("button").length){
$w.$titlebar.on("pointerdown", (e) => {
if ($(e.target).closest("button").length) {
return;
}
if ($w.hasClass("maximized")) {
return;
}
const customEvent = $.Event("window-drag-start");
$w.trigger(customEvent);
if (customEvent.isDefaultPrevented()) {
return; // allow custom drag behavior of component windows in jspaint (Tools / Colors)
}
drag_offset_x = e.clientX + scrollX - $w.position().left;
drag_offset_y = e.clientY + scrollY - $w.position().top;
drag_pointer_x = e.clientX;
drag_pointer_y = e.clientY;
drag_pointer_id = e.pointerId;
$G.on("pointermove", update_drag);
$G.on("scroll", update_drag);
$("body").addClass("dragging"); // for when mouse goes over an iframe
});
$G.on("pointerup", function(e){
$G.on("pointerup pointercancel", (e) => {
if (e.pointerId !== drag_pointer_id) { return; }
$G.off("pointermove", update_drag);
$G.off("scroll", update_drag);
$("body").removeClass("dragging");
// $w.applyBounds(); // Windows doesn't really try to keep windows on screen
// but you also can't really drag off of the desktop, whereas here you can drag to way outside the web page.
$w.bringTitleBarOnScreen();
$w.bringTitleBarInBounds();
});
$w.$titlebar.on("dblclick", function(e){
if($component){
$w.$titlebar.on("dblclick", (e) => {
if ($component) {
$component.dock();
}
});
$w.$Button = function(text, handler){
if (options.resizable) {
const HANDLE_MIDDLE = 0;
const HANDLE_START = -1;
const HANDLE_END = 1;
const HANDLE_LEFT = HANDLE_START;
const HANDLE_RIGHT = HANDLE_END;
const HANDLE_TOP = HANDLE_START;
const HANDLE_BOTTOM = HANDLE_END;
[
[HANDLE_TOP, HANDLE_RIGHT], // ↗
[HANDLE_TOP, HANDLE_MIDDLE], // ↑
[HANDLE_TOP, HANDLE_LEFT], // ↖
[HANDLE_MIDDLE, HANDLE_LEFT], // ←
[HANDLE_BOTTOM, HANDLE_LEFT], // ↙
[HANDLE_BOTTOM, HANDLE_MIDDLE], // ↓
[HANDLE_BOTTOM, HANDLE_RIGHT], // ↘
[HANDLE_MIDDLE, HANDLE_RIGHT], // →
].forEach(([y_axis, x_axis]) => {
// const resizes_height = y_axis !== HANDLE_MIDDLE;
// const resizes_width = x_axis !== HANDLE_MIDDLE;
const $handle = $("<div>").addClass("handle").appendTo($w);
let cursor = "";
if (y_axis === HANDLE_TOP) { cursor += "n"; }
if (y_axis === HANDLE_BOTTOM) { cursor += "s"; }
if (x_axis === HANDLE_LEFT) { cursor += "w"; }
if (x_axis === HANDLE_RIGHT) { cursor += "e"; }
cursor += "-resize";
// Note: innerWidth() is less "inner" than width(), because it includes padding!
// Here's a little diagram of sorts:
// outerWidth(true): margin, [ outerWidth(): border, [ innerWidth(): padding, [ width(): content ] ] ]
const handle_thickness = ($w.outerWidth() - $w.width()) / 2; // padding + border
const border_width = ($w.outerWidth() - $w.innerWidth()) / 2; // border; need to outset the handles by this amount so they overlap the border + padding, and not the content
const window_frame_height = $w.outerHeight() - $w.$content.outerHeight(); // includes titlebar and borders
$handle.css({
position: "absolute",
top: y_axis === HANDLE_TOP ? -border_width : y_axis === HANDLE_MIDDLE ? `calc(${handle_thickness}px - ${border_width}px)` : "",
bottom: y_axis === HANDLE_BOTTOM ? -border_width : "",
left: x_axis === HANDLE_LEFT ? -border_width : x_axis === HANDLE_MIDDLE ? `calc(${handle_thickness}px - ${border_width}px)` : "",
right: x_axis === HANDLE_RIGHT ? -border_width : "",
width: x_axis === HANDLE_MIDDLE ? `calc(100% - ${handle_thickness}px * 2 + ${border_width * 2}px)` : `${handle_thickness}px`,
height: y_axis === HANDLE_MIDDLE ? `calc(100% - ${handle_thickness}px * 2 + ${border_width * 2}px)` : `${handle_thickness}px`,
// background: x_axis === HANDLE_MIDDLE || y_axis === HANDLE_MIDDLE ? "rgba(255,0,0,0.4)" : "rgba(0,255,0,0.8)",
touchAction: "none",
cursor,
});
let rect;
let resize_offset_x, resize_offset_y, resize_pointer_x, resize_pointer_y, resize_pointer_id;
$handle.on("pointerdown", (e) => {
e.preventDefault();
$G.on("pointermove", handle_pointermove);
$G.on("scroll", update_resize); // scroll doesn't have clientX/Y, so we have to remember it
$("body").addClass("dragging"); // for when mouse goes over an iframe
$G.on("pointerup pointercancel", end_resize);
rect = {
x: $w.position().left,
y: $w.position().top,
width: $w.outerWidth(),
height: $w.outerHeight(),
};
resize_offset_x = e.clientX + scrollX - rect.x - (x_axis === HANDLE_RIGHT ? rect.width : 0);
resize_offset_y = e.clientY + scrollY - rect.y - (y_axis === HANDLE_BOTTOM ? rect.height : 0);
resize_pointer_x = e.clientX;
resize_pointer_y = e.clientY;
resize_pointer_id = e.pointerId;
// handle_pointermove(e); // was useful for checking that the offset is correct (should not do anything, if it's correct!)
});
function handle_pointermove(e) {
if (e.pointerId !== resize_pointer_id) { return; }
resize_pointer_x = e.clientX;
resize_pointer_y = e.clientY;
update_resize();
}
function end_resize(e) {
if (e.pointerId !== resize_pointer_id) { return; }
$G.off("pointermove", handle_pointermove);
$G.off("scroll", onscroll);
$("body").removeClass("dragging");
$G.off("pointerup pointercancel", end_resize);
$w.bringTitleBarInBounds();
}
function update_resize() {
const mouse_x = resize_pointer_x + scrollX - resize_offset_x;
const mouse_y = resize_pointer_y + scrollY - resize_offset_y;
let delta_x = 0;
let delta_y = 0;
let width, height;
if (x_axis === HANDLE_RIGHT) {
delta_x = 0;
width = ~~(mouse_x - rect.x);
} else if (x_axis === HANDLE_LEFT) {
delta_x = ~~(mouse_x - rect.x);
width = ~~(rect.x + rect.width - mouse_x);
} else {
width = ~~(rect.width);
}
if (y_axis === HANDLE_BOTTOM) {
delta_y = 0;
height = ~~(mouse_y - rect.y);
} else if (y_axis === HANDLE_TOP) {
delta_y = ~~(mouse_y - rect.y);
height = ~~(rect.y + rect.height - mouse_y);
} else {
height = ~~(rect.height);
}
let new_rect = {
x: rect.x + delta_x,
y: rect.y + delta_y,
width,
height,
};
new_rect.width = Math.max(1, new_rect.width);
new_rect.height = Math.max(1, new_rect.height);
// Constraints
if (options.constrainRect) {
new_rect = options.constrainRect(new_rect, x_axis, y_axis);
}
new_rect.width = Math.max(new_rect.width, options.minWidth ?? 100);
new_rect.height = Math.max(new_rect.height, options.minHeight ?? window_frame_height);
// prevent free movement via resize past minimum size
if (x_axis === HANDLE_LEFT) {
new_rect.x = Math.min(new_rect.x, rect.x + rect.width - new_rect.width);
}
if (y_axis === HANDLE_TOP) {
new_rect.y = Math.min(new_rect.y, rect.y + rect.height - new_rect.height);
}
$w.css({
top: new_rect.y,
left: new_rect.x,
});
$w.outerWidth(new_rect.width);
$w.outerHeight(new_rect.height);
}
});
}
$w.$Button = (text, handler) => {
var $b = $(E("button"))
.appendTo($w.$content)
.text(text)
.on("click", function(){
if(handler){
.on("click", () => {
if (handler) {
handler();
}
$w.close();
});
return $b;
};
$w.title = function(title){
if(title !== undefined){
$w.title = title => {
if (title) {
$w.$title.text(title);
if ($w.task) {
$w.task.updateTitle();
}
return $w;
}else{
} else {
return $w.$title.text();
}
};
$w.getTitle = function() {
$w.getTitle = () => {
return $w.title();
};
$w.getIconName = function() {
$w.getIconName = () => {
return $w.icon_name;
};
$w.setIconByID = function(icon_name){
$w.setIconByID = (icon_name) => {
// $w.$icon.attr("src", getIconPath(icon_name));
var old_$icon = $w.$icon;
$w.$icon = $Icon(icon_name, TITLEBAR_ICON_SIZE);
@ -371,14 +642,14 @@ function $Window(options){
$w.task.updateIcon();
return $w;
};
$w.animateTitlebar = function(from, to, callback=()=>{}) {
$w.animateTitlebar = (from, to, callback = () => { }) => {
const $eye_leader = $w.$titlebar.clone(true);
$eye_leader.find("button").remove();
$eye_leader.appendTo("body");
const durationMS = 200; // TODO: how long?
const duration = `${durationMS}ms`;
const duration_ms = 200; // TODO: how long?
const duration_str = `${duration_ms}ms`;
$eye_leader.css({
transition: `left ${duration} linear, top ${duration} linear, width ${duration} linear, height ${duration} linear`,
transition: `left ${duration_str} linear, top ${duration_str} linear, width ${duration_str} linear, height ${duration_str} linear`,
position: "fixed",
zIndex: 10000000,
pointerEvents: "none",
@ -387,7 +658,7 @@ function $Window(options){
width: from.width,
height: from.height,
});
setTimeout(()=> {
setTimeout(() => {
$eye_leader.css({
left: to.left,
top: to.top,
@ -395,71 +666,77 @@ function $Window(options){
height: to.height,
});
}, 5);
const tid = setTimeout(()=> {
const tid = setTimeout(() => {
$eye_leader.remove();
callback();
}, durationMS * 1.2);
$eye_leader.on("transitionend animationcancel", ()=> {
}, duration_ms * 1.2);
$eye_leader.on("transitionend animationcancel", () => {
$eye_leader.remove();
clearTimeout(tid);
callback();
});
};
$w.close = function(force){
if(!force){
$w.close = (force) => {
if (!force) {
var e = $.Event("close");
$w.trigger(e);
if(e.isDefaultPrevented()){
if (e.isDefaultPrevented()) {
return;
}
}
if($component){
if ($component) {
$component.detach();
}
$w.remove();
$w.closed = true;
$eventTarget.triggerHandler("closed");
// $w.trigger("closed");
$event_target.triggerHandler("closed");
$w.trigger("closed");
// TODO: change usages of "close" to "closed" where appropriate
// and probably rename the "close" event
// Focus next-topmost window
// TODO: store the last focused control OUTSIDE the window, and restore it here,
// so that it works with not just other windows but also arbitrary controls outside of any window.
var $next_topmost = $($(".window:visible").toArray().sort((a, b) => b.style.zIndex - a.style.zIndex)[0]);
$next_topmost.triggerHandler("refocus-window");
};
$w.closed = false;
if(options.title){
if (options.title) {
$w.title(options.title);
}
if(!$component){
if (!$component) {
$w.center();
}
// mustHaveMethods($w, windowInterfaceMethods);
return $w;
}
function $FormWindow(title){
function $FormWindow(title) {
var $w = new $Window();
$w.title(title);
$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 = function(label, action){
$w.$Button = (label, action) => {
var $b = $(E("button")).appendTo($w.$buttons).text(label);
$b.on("click", function(e){
$b.on("click", (e) => {
// prevent the form from submitting
// @TODO: instead, prevent the form's submit event
e.preventDefault();
action();
});
$b.on("pointerdown", function(){
$b.on("pointerdown", () => {
$b.focus();
});
return $b;
};

File diff suppressed because one or more lines are too long

View File

@ -7,8 +7,24 @@
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
text-align: start;
user-select: none;
}
.os-window {
contain: layout; /* contain: paint; seems to clip children to the padding-box, including for interaction, not just painting; it breaks being able to grab resize handles over the border */
/* overflow: hidden; is also not usable for the same reason */
/* I might be able to do either with overflow-clip-margin however (@TODO) */
display: flex;
flex-direction: column;
/* will-change: width height left top; */
}
.window-content {
flex: 1;
min-height: 0px;
/* We want to allow scrollbars to be enabled easily, but not enable them,
and we want to clip to the border of the window, without overlapping it at all.
It's already clipped to the outside of .os-window's border, but we want to clip to the inside. */
contain: layout paint;
}
.os-window .window-titlebar,

View File

@ -1 +1 @@
{"version":3,"sources":["../src/layout.css"],"names":[],"mappings":"AAAA;;;;;CAKC,eAAe;CACf,yBAAiB;IAAjB,sBAAiB;KAAjB,qBAAiB;SAAjB,iBAAiB;AAClB;;AAEA;;CAEC,aAAa;CACb,mBAAmB;CACnB,mBAAmB;AACpB;AACA;CACC,kBAAkB;CAClB,OAAO;AACR;;AAEA;CACC,sBAAsB;AACvB;;AAEA,oDAAoD;AACpD;CACC,oBAAoB;AACrB;;AAEA;CACC,aAAa;CACb,cAAc;AACf;AACA;CACC,kBAAkB;AACnB;AACA;CACC,kBAAkB;CAClB,SAAS;CACT,OAAO;CACP,eAAe,EAAE,sDAAsD;CACvE,sBAAsB;AACvB;AACA;CACC,yBAAyB;AAC1B;AACA;CACC,mBAAmB;AACpB;AACA;CACC,yBAAyB;CACzB,SAAS;CACT,WAAW;AACZ;AACA;CACC,0BAA0B;AAC3B;AACA;;CAEC,eAAe;AAChB;AACA;;CAEC,kBAAkB;AACnB;;AAEA;;;;;;GAMG;;AAEH;;CAEC,oBAAoB;AACrB","file":"layout.css","sourcesContent":[".menus,\r\n.menu-popup,\r\n.os-window,\r\n.os-window .window-titlebar,\r\n.os-window .window-title {\r\n\tcursor: default;\r\n\tuser-select: none;\r\n}\r\n\r\n.os-window .window-titlebar,\r\nbody > .window-titlebar {\r\n\tdisplay: flex;\r\n\tflex-direction: row;\r\n\talign-items: center;\r\n}\r\n.os-window .window-title-area {\r\n\tposition: relative;\r\n\tflex: 1;\r\n}\r\n\r\n.os-window .window-titlebar .icon {\r\n\tvertical-align: bottom;\r\n}\r\n\r\n/* Fix dragging things (like windows) over iframes */\r\n.dragging iframe {\r\n\tpointer-events: none;\r\n}\r\n\r\n.menus {\r\n\tdisplay: flex;\r\n\tflex: 0 0 auto;\r\n}\r\n.menu-container {\r\n\tposition: relative;\r\n}\r\n.menu-popup {\r\n\tposition: absolute;\r\n\ttop: 100%;\r\n\tleft: 0;\r\n\tz-index: 400000; /* so sub-menu-popups can be visible over the window */\r\n\tbox-sizing: border-box;\r\n}\r\n.menu-popup-table {\r\n\tborder-collapse: collapse;\r\n}\r\n.menu-item {\r\n\twhite-space: nowrap;\r\n}\r\n.menu-hr {\r\n\tdisplay: block !important;\r\n\theight: 0;\r\n\twidth: auto;\r\n}\r\n.menu-hotkey {\r\n\tdisplay: inline !important;\r\n}\r\n.menu-item-checkbox-area,\r\n.menu-item-submenu-area {\r\n\tmin-width: 16px;\r\n}\r\n.menu-item-checkbox-area,\r\n.menu-item-submenu-area {\r\n\ttext-align: center;\r\n}\r\n\r\n/* .window-content .button-group {\r\n\twidth: 85px;\r\n}\r\n.window-content .button-group > button {\r\n\twidth: 95%;\r\n\tpadding: 3px 5px;\r\n} */\r\n\r\n::before,\r\n::after {\r\n\tpointer-events: none;\r\n}\r\n"]}
{"version":3,"sources":["../src/layout.css"],"names":[],"mappings":"AAAA;;;;;CAKC,eAAe;CACf,yBAAiB;IAAjB,sBAAiB;KAAjB,qBAAiB;SAAjB,iBAAiB;AAClB;;AAEA;CACC,eAAe,EAAE,yKAAyK;CAC1L,6DAA6D;CAC7D,2EAA2E;CAC3E,aAAa;CACb,sBAAsB;CACtB,wCAAwC;AACzC;AACA;CACC,OAAO;CACP,eAAe;CACf;;iGAEgG;IAC7F,qBAAqB;AACzB;;AAEA;;CAEC,aAAa;CACb,mBAAmB;CACnB,mBAAmB;AACpB;AACA;CACC,kBAAkB;CAClB,OAAO;AACR;;AAEA;CACC,sBAAsB;AACvB;;AAEA,oDAAoD;AACpD;CACC,oBAAoB;AACrB;;AAEA;CACC,aAAa;CACb,cAAc;AACf;AACA;CACC,kBAAkB;AACnB;AACA;CACC,kBAAkB;CAClB,SAAS;CACT,OAAO;CACP,eAAe,EAAE,sDAAsD;CACvE,sBAAsB;AACvB;AACA;CACC,yBAAyB;AAC1B;AACA;CACC,mBAAmB;AACpB;AACA;CACC,yBAAyB;CACzB,SAAS;CACT,WAAW;AACZ;AACA;CACC,0BAA0B;AAC3B;AACA;;CAEC,eAAe;AAChB;AACA;;CAEC,kBAAkB;AACnB;;AAEA;;;;;;GAMG;;AAEH;;CAEC,oBAAoB;AACrB","file":"layout.css","sourcesContent":[".menus,\n.menu-popup,\n.os-window,\n.os-window .window-titlebar,\n.os-window .window-title {\n\tcursor: default;\n\tuser-select: none;\n}\n\n.os-window {\n\tcontain: layout; /* contain: paint; seems to clip children to the padding-box, including for interaction, not just painting; it breaks being able to grab resize handles over the border */\n\t/* overflow: hidden; is also not usable for the same reason */\n\t/* I might be able to do either with overflow-clip-margin however (@TODO) */\n\tdisplay: flex;\n\tflex-direction: column;\n\t/* will-change: width height left top; */\n}\n.window-content {\n\tflex: 1;\n\tmin-height: 0px;\n\t/* We want to allow scrollbars to be enabled easily, but not enable them,\n\tand we want to clip to the border of the window, without overlapping it at all.\n\tIt's already clipped to the outside of .os-window's border, but we want to clip to the inside. */\n contain: layout paint;\n}\n\n.os-window .window-titlebar,\nbody > .window-titlebar {\n\tdisplay: flex;\n\tflex-direction: row;\n\talign-items: center;\n}\n.os-window .window-title-area {\n\tposition: relative;\n\tflex: 1;\n}\n\n.os-window .window-titlebar .icon {\n\tvertical-align: bottom;\n}\n\n/* Fix dragging things (like windows) over iframes */\n.dragging iframe {\n\tpointer-events: none;\n}\n\n.menus {\n\tdisplay: flex;\n\tflex: 0 0 auto;\n}\n.menu-container {\n\tposition: relative;\n}\n.menu-popup {\n\tposition: absolute;\n\ttop: 100%;\n\tleft: 0;\n\tz-index: 400000; /* so sub-menu-popups can be visible over the window */\n\tbox-sizing: border-box;\n}\n.menu-popup-table {\n\tborder-collapse: collapse;\n}\n.menu-item {\n\twhite-space: nowrap;\n}\n.menu-hr {\n\tdisplay: block !important;\n\theight: 0;\n\twidth: auto;\n}\n.menu-hotkey {\n\tdisplay: inline !important;\n}\n.menu-item-checkbox-area,\n.menu-item-submenu-area {\n\tmin-width: 16px;\n}\n.menu-item-checkbox-area,\n.menu-item-submenu-area {\n\ttext-align: center;\n}\n\n/* .window-content .button-group {\n\twidth: 85px;\n}\n.window-content .button-group > button {\n\twidth: 95%;\n\tpadding: 3px 5px;\n} */\n\n::before,\n::after {\n\tpointer-events: none;\n}\n"]}

View File

@ -7,8 +7,24 @@
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
text-align: start;
user-select: none;
}
.os-window {
contain: layout; /* contain: paint; seems to clip children to the padding-box, including for interaction, not just painting; it breaks being able to grab resize handles over the border */
/* overflow: hidden; is also not usable for the same reason */
/* I might be able to do either with overflow-clip-margin however (@TODO) */
display: flex;
flex-direction: column;
/* will-change: width height left top; */
}
.window-content {
flex: 1;
min-height: 0px;
/* We want to allow scrollbars to be enabled easily, but not enable them,
and we want to clip to the border of the window, without overlapping it at all.
It's already clipped to the outside of .os-window's border, but we want to clip to the inside. */
contain: layout paint;
}
.os-window .window-titlebar,

File diff suppressed because one or more lines are too long

View File

@ -258,9 +258,7 @@ body > .window-titlebar {
font-family: 'Segoe UI', sans-serif;
font-size: 12px;
}
.os-window .window-titlebar,
body > .window-titlebar {
.os-window:not(.tool-window) .window-titlebar {
font-weight: bold;
}
.os-window:not(.focused) .window-titlebar {
@ -281,7 +279,7 @@ body > .window-titlebar {
/*color: var(--WindowText);*/
/*border: 1px solid var(--WindowFrame);*/
/* TODO: use window-specific theme colors; also different types of windows */
/* padding: 2px; */
padding: 2px;
}
.os-window:not(.maximized) {
border-style: solid;
@ -314,6 +312,14 @@ body > .window-titlebar {
margin-left: 2px;
margin-right: 2px;
}
.tool-window .window-close-button {
width: 13px;
height: 11px;
background-position: 8px 0;
}
.tool-window .window-close-button:hover:active {
background-position: 9px 1px;
}
.os-window .window-title-area {
height: 16px; /* 100% doesn't work */
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,299 +1,7 @@
$Window.Z_INDEX = 50; // dynamically incrementing z-index
function make_window_supporting_scale(options) {
const $w = new $Window(options);
function $ToolWindow($component){
const $w = $(E("div")).addClass("window tool-window").appendTo("body");
$w.$titlebar = $(E("div")).addClass("window-titlebar").appendTo($w);
$w.$title = $(E("span")).addClass("window-title").appendTo($w.$titlebar);
$w.$x = $(E("button")).addClass("window-close-button window-button").appendTo($w.$titlebar);
$w.$content = $(E("div")).addClass("window-content").appendTo($w);
if($component){
$w.addClass("component-window");
}
$w.css("touch-action", "none");
$w.$x.on("click", () => {
$w.close();
});
$w.$x.on("mousedown selectstart", e => {
e.preventDefault();
});
// @TODO: prevent selection *outside* of the window *via* the window
$w.css({
position: "absolute",
zIndex: $Window.Z_INDEX++
});
// var focused = false;
var last_focused_control;
$w.on("pointerdown refocus-window", (event) => {
$w.css({
zIndex: $Window.Z_INDEX++
});
// Test cases where it should refocus the last focused control in the window:
// - Click in the blank space of the window
// - Click on the window title bar
// - Close a second window, focusing the first window
// - Clicking on a control in the window should focus it, by way of updating last_focused_control
// - Eye gaze mode dwell click (simulated click)
// It should NOT refocus when:
// - (Clicking on a control in a different window)
// - Trying to select text!
// Wait for other pointerdown handlers and default behavior, and focusin events.
// Set focus to the last focused control, which should be updated if a click just occurred.
requestAnimationFrame(()=> {
// focused = true;
// But if the element is selectable, wait until the click is done and see if anything was selected first.
// This is a bit of a weird compromise, for now.
const target_style = getComputedStyle(event.target);
if (target_style.userSelect !== "none") {
$w.one("pointerup", () => {
requestAnimationFrame(()=> { // this seems to make it more reliable in regards to double clicking
if (last_focused_control && !getSelection().toString().trim()) {
last_focused_control.focus();
}
});
});
return;
}
if (last_focused_control) {
last_focused_control.focus();
}
});
});
// Assumption: no control exists in the window before, this "focusin" handler is set up,
// so any element.focus() will be after and trigger this handler.
$w.on("focusin", ()=> {
// focused = true;
if (document.activeElement && $.contains($w[0], document.activeElement)) {
last_focused_control = document.activeElement;
}
});
// $w.on("focusout", ()=> {
// requestAnimationFrame(()=> {
// if (!document.activeElement || !$.contains($w[0], document.activeElement)) {
// focused = false;
// }
// });
// });
$w.on("keydown", e => {
if(e.ctrlKey || e.altKey || e.metaKey){
return;
}
const $buttons = $w.$content.find("button");
const $focused = $(document.activeElement);
const focused_index = $buttons.index($focused);
switch(e.keyCode){
case 40: // Down
case 39: // Right
if($focused.is("button") && !e.shiftKey){
if(focused_index < $buttons.length - 1){
$buttons[focused_index + 1].focus();
e.preventDefault();
}
}
break;
case 38: // Up
case 37: // Left
if($focused.is("button") && !e.shiftKey){
if(focused_index > 0){
$buttons[focused_index - 1].focus();
e.preventDefault();
}
}
break;
case 32: // Space
case 13: // Enter (doesn't actually work in chrome because the button gets clicked immediately)
if($focused.is("button") && !e.shiftKey){
$focused.addClass("pressed");
const release = () => {
$focused.removeClass("pressed");
$focused.off("focusout", release);
$(window).off("keyup", keyup);
};
const keyup = e => {
if(e.keyCode === 32 || e.keyCode === 13){
release();
}
};
$focused.on("focusout", release);
$(window).on("keyup", keyup);
}
break;
case 9: { // Tab
// wrap around when tabbing through controls in a window
// @#: focusables
let $controls = $w.$content.find("input, textarea, select, button, object, a[href], [tabIndex='0'], details summary").filter(":enabled, summary, a").filter(":visible");
// const $controls = $w.$content.find(":tabbable"); // https://api.jqueryui.com/tabbable-selector/
// Radio buttons should be treated as a group with one tabstop.
// If there's no selected ("checked") radio, it should still visit the group,
// but it should skip all unselected radios in that group if there is a selected radio in that group.
const radios = {}; // best radio found so far, per group
const toSkip = [];
for (const el of $controls) {
if (el.nodeName.toLowerCase() === "input" && el.type === "radio") {
if (radios[el.name]) {
if (el.checked) {
toSkip.push(radios[el.name]);
radios[el.name] = el;
} else {
toSkip.push(el);
}
} else {
radios[el.name] = el;
}
}
}
$controls = $controls.not(toSkip);
// debug viz:
// $controls.css({boxShadow: "0 0 2px 2px green"});
// $(toSkip).css({boxShadow: "0 0 2px 2px gray"})
if ($controls.length > 0) {
const focused_control_index = $controls.index($focused);
if (e.shiftKey) {
if(focused_control_index === 0){
e.preventDefault();
$controls[$controls.length - 1].focus();
}
} else {
if(focused_control_index === $controls.length - 1){
e.preventDefault();
$controls[0].focus();
}
}
}
break;
}
case 27: // Esc
$w.close();
break;
}
});
$w.applyBounds = () => {
$w.css({
left: Math.max(0, Math.min(innerWidth - $w.outerWidth(), $w[0].getBoundingClientRect().left)),
top: Math.max(0, Math.min(innerHeight - $w.outerHeight(), $w[0].getBoundingClientRect().top)),
});
};
$w.bringTitleBarOnScreen = () => {
// Try to make the titlebar always accessible
const min_horizontal_pixels_on_screen = 40; // enough for space past a close button
$w.css({
left: Math.max(
min_horizontal_pixels_on_screen - $w.outerWidth(),
Math.min(
innerWidth - min_horizontal_pixels_on_screen,
$w[0].getBoundingClientRect().left
)
),
top: Math.max(0, Math.min(
innerHeight - $w.$titlebar.outerHeight() - 5,
$w[0].getBoundingClientRect().top
)),
});
};
$w.center = () => {
$w.css({
left: (innerWidth - $w.outerWidth()) / 2,
top: (innerHeight - $w.outerHeight()) / 2,
});
$w.applyBounds();
};
$G.on("resize", $w.bringTitleBarOnScreen);
let drag_offset_x, drag_offset_y;
const drag = e => {
$w.css({
left: e.clientX - drag_offset_x,
top: e.clientY - drag_offset_y,
});
};
$w.$titlebar.css("touch-action", "none");
$w.$titlebar.on("mousedown selectstart", e => {
e.preventDefault();
});
$w.$titlebar.on("pointerdown", e => {
if(!$w.$titlebar.is(e.target)){
return; // don't drag via buttons
}
// if (e.isDefaultPrevented()) { // doesn't work because of event listener order
// return; // allow custom drag behavior of component windows (Tools / Colors)
// }
const customEvent = $.Event("window-drag-start");
$w.trigger(customEvent);
if(customEvent.isDefaultPrevented()){
return; // allow custom drag behavior of component windows (Tools / Colors)
}
drag_offset_x = e.clientX - $w[0].getBoundingClientRect().left;
drag_offset_y = e.clientY - $w[0].getBoundingClientRect().top;
$G.on("pointermove", drag);
$("body").addClass("dragging");
const stop_drag = ()=> {
// $w.applyBounds(); // Windows doesn't really try to keep windows on screen
// but you also can't really drag off of the desktop, whereas here you can drag to way outside the web page.
$w.bringTitleBarOnScreen();
$G.off("pointermove", drag);
$G.off("pointerup pointercancel", stop_drag);
$("body").removeClass("dragging");
};
$G.on("pointerup pointercancel", stop_drag);
});
$w.$titlebar.on("dblclick", ()=> {
if($component){
$component.dock();
}
});
$w.$Button = (text, handler) => {
const $b = $(E("button"))
.appendTo($w.$content)
.text(text)
.on("click", () => {
if(handler){
handler();
}
$w.close();
});
return $b;
};
$w.title = title => {
if(title){
$w.$title.text(title);
return $w;
}else{
return $w.$title.text();
}
};
$w.close = () => {
const e = $.Event("close");
$w.trigger(e);
if(e.isDefaultPrevented()){
return;
}
if($component){
$component.detach();
}
$w.remove();
$w.closed = true;
// Focus next-topmost window
$(
$(".window:visible").toArray().sort((a, b)=> b.style.zIndex - a.style.zIndex)[0]
).triggerHandler("refocus-window");
};
$w.closed = false;
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;
@ -328,7 +36,7 @@ function $ToolWindow($component){
// requestAnimationFrame(scale_for_eye_gaze_mode_and_center);
};
if(!$component){
if(!options.$component){
$w.center();
const scale_for_eye_gaze_mode_and_center_next_frame = ()=> {
@ -343,34 +51,53 @@ function $ToolWindow($component){
scale_for_eye_gaze_mode_and_center_next_frame();
}
if (options.$component) {
$w.$content.css({
contain: "none",
});
}
return $w;
}
function $FormToolWindow(title){
const $w = new $ToolWindow();
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.title(title);
$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 => {
$b.on("click", (e) => {
// prevent the form from submitting
// @TODO: instead prevent submit event
// @TODO: instead, prevent the form's submit event
e.preventDefault();
action();
});
$b.on("pointerdown", () => {
$b[0].focus();
$b.focus();
});
return $b;
};
return $w;
}

View File

@ -1088,7 +1088,7 @@ function loaded_localizations(language, mapping) {
current_language = language;
}
function set_language(language) {
const $w = $FormToolWindow().title("Reload Required").addClass("dialogue-window");
const $w = $DialogWindow().title("Reload Required").addClass("dialogue-window");
$w.$main.text("The application needs to reload to change the language.");
$w.$main.css("max-width", "600px");
$w.$Button(localize("OK"), () => {

View File

@ -150,7 +150,7 @@ function choose_color(initial_color, callback) {
if ($edit_colors_window) {
$edit_colors_window.close();
}
const $w = new $FormToolWindow(localize("Edit Colors"));
const $w = new $DialogWindow(localize("Edit Colors"));
$w.addClass("edit-colors-window");
$edit_colors_window = $w;

View File

@ -308,7 +308,7 @@ function show_custom_zoom_window() {
if ($custom_zoom_window) {
$custom_zoom_window.close();
}
const $w = new $FormToolWindow(localize("Custom Zoom"));
const $w = new $DialogWindow(localize("Custom Zoom"));
$custom_zoom_window = $w;
$w.addClass("custom-zoom-window");
@ -794,7 +794,7 @@ function file_load_from_url(){
if($file_load_from_url_window){
$file_load_from_url_window.close();
}
const $w = new $FormToolWindow().addClass("dialogue-window");
const $w = new $DialogWindow().addClass("dialogue-window");
$file_load_from_url_window = $w;
$w.title("Load from URL");
// @TODO: URL validation (input has to be in a form (and we don't want the form to submit))
@ -838,7 +838,7 @@ function confirm_overwrite() {
resolve();
return;
}
const $w = new $FormToolWindow().addClass("dialogue-window");
const $w = new $DialogWindow().addClass("dialogue-window");
$w.title(localize("Paint"));
$w.$main.html(`
<p>JS Paint can now save over existing files.</p>
@ -915,7 +915,7 @@ function are_you_sure(action, canceled){
if(saved){
action();
}else{
const $w = new $FormToolWindow().addClass("dialogue-window");
const $w = new $DialogWindow().addClass("dialogue-window");
$w.title(localize("Paint"));
$w.$main.text(localize("Save changes to %1?", file_name));
$w.$Button(localize("Save"), () => {
@ -940,7 +940,7 @@ function are_you_sure(action, canceled){
}
function please_enter_a_number() {
const $w = new $FormToolWindow("Invalid Value").addClass("dialogue-window");
const $w = new $DialogWindow("Invalid Value").addClass("dialogue-window");
$w.$main.text(localize("Please enter a number."));
$w.$Button(localize("OK"), () => {
$w.close();
@ -948,7 +948,7 @@ function please_enter_a_number() {
}
function show_error_message(message, error){
const $w = $FormToolWindow().title(localize("Paint")).addClass("dialogue-window squish");
const $w = $DialogWindow().title(localize("Paint")).addClass("dialogue-window squish");
$w.$main.text(message);
$w.$main.css("max-width", "600px");
if(error){
@ -996,7 +996,7 @@ function show_error_message(message, error){
// @TODO: close are_you_sure windows and these Error windows when switching sessions
// because it can get pretty confusing
function show_resource_load_error_message(error){
const $w = $FormToolWindow().title(localize("Paint")).addClass("dialogue-window");
const $w = $DialogWindow().title(localize("Paint")).addClass("dialogue-window");
const firefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
// @TODO: copy & paste vs download & open, more specific guidance
if (error.code === "cross-origin-blob-uri") {
@ -1049,7 +1049,7 @@ function show_resource_load_error_message(error){
$w.center();
}
function show_file_format_errors({ as_image_error, as_palette_error }) {
const $w = $FormToolWindow().title(localize("Paint")).addClass("dialogue-window");
const $w = $DialogWindow().title(localize("Paint")).addClass("dialogue-window");
let html = `
<p>${localize("Paint cannot open this file.")}</p>
`;
@ -1131,7 +1131,12 @@ function show_about_paint(){
if($about_paint_window){
$about_paint_window.close();
}
$about_paint_window = $ToolWindow().title(localize("About Paint"));
$about_paint_window = $Window({
title: localize("About Paint"),
resizable: false,
maximizeButton: false,
minimizeButton: false,
});
$about_paint_window.addClass("about-paint squish");
if (is_pride_month) {
$("#paint-32x32").attr("src", "./images/icons/gay-es-paint-32x32-light-outline.png");
@ -1244,7 +1249,12 @@ function show_news(){
if($news_window){
$news_window.close();
}
$news_window = $ToolWindow().title("Project News");
$news_window = $Window({
title: "Project News",
maximizeButton: false,
minimizeButton: false,
resizable: false,
});
$news_window.addClass("news-window squish");
@ -1291,7 +1301,7 @@ async function choose_file_to_paste() {
function paste(img_or_canvas){
if(img_or_canvas.width > main_canvas.width || img_or_canvas.height > main_canvas.height){
const $w = new $FormToolWindow().addClass("dialogue-window");
const $w = new $DialogWindow().addClass("dialogue-window");
$w.title(localize("Paint"));
$w.$main.html(`
${localize("The image in the clipboard is larger than the bitmap.")}<br>
@ -1350,7 +1360,7 @@ function paste(img_or_canvas){
}
function render_history_as_gif(){
const $win = $FormToolWindow();
const $win = $DialogWindow();
$win.title("Rendering GIF");
const $output = $win.$main;
@ -1645,9 +1655,9 @@ function redo(){
$document_history_prompt_window.close();
}
if (!$document_history_window || $document_history_window.closed) {
const $w = $document_history_prompt_window = new $ToolWindow();
const $w = $document_history_prompt_window = new $DialogWindow();
$w.title("Redo");
$w.$content.html("To view all branches of the history tree, click Edit > History.").css({padding: 10});
$w.$main.html("To view all branches of the history tree, click Edit > History.").css({padding: 10});
// $w.$Button("Show History", show_document_history).css({margin: 10}).focus();
// $w.$Button(localize("Cancel"), ()=> { $w.close(); }).css({margin: 10});
$w.$Button(localize("OK"), ()=> { $w.close(); }).css({margin: 10});
@ -1686,9 +1696,13 @@ function show_document_history() {
if ($document_history_window) {
$document_history_window.close();
}
const $w = $document_history_window = new $ToolWindow();
const $w = $document_history_window = new $Window({
title: "Document History",
resizable: false,
maximizeButton: false,
minimizeButton: false,
});
// $w.prependTo("body").css({position: ""});
$w.title("Document History");
$w.addClass("history-window squish");
$w.$content.html(`
<div class="history-view" tabIndex="0"></div>
@ -2332,7 +2346,7 @@ function image_attributes(){
if(image_attributes.$window){
image_attributes.$window.close();
}
const $w = image_attributes.$window = new $FormToolWindow(localize("Attributes"));
const $w = image_attributes.$window = new $DialogWindow(localize("Attributes"));
$w.addClass("attributes-window");
const $main = $w.$main;
@ -2488,7 +2502,7 @@ function image_attributes(){
}
function show_convert_to_black_and_white() {
const $w = new $FormToolWindow("Convert to Black and White");
const $w = new $DialogWindow("Convert to Black and White");
$w.addClass("convert-to-black-and-white");
$w.$main.append("<fieldset><legend>Threshold:</legend><input type='range' min='0' max='1' step='0.01' value='0.5'></fieldset>");
const $slider = $w.$main.find("input[type='range']");
@ -2528,7 +2542,7 @@ function show_convert_to_black_and_white() {
}
function image_flip_and_rotate(){
const $w = new $FormToolWindow(localize("Flip and Rotate"));
const $w = new $DialogWindow(localize("Flip and Rotate"));
$w.addClass("flip-and-rotate");
const $fieldset = $(E("fieldset")).appendTo($w.$main);
@ -2675,7 +2689,7 @@ function image_flip_and_rotate(){
}
function image_stretch_and_skew(){
const $w = new $FormToolWindow(localize("Stretch and Skew"));
const $w = new $DialogWindow(localize("Stretch and Skew"));
$w.addClass("stretch-and-skew");
const $fieldset_stretch = $(E("fieldset")).appendTo($w.$main);
@ -2776,7 +2790,7 @@ function save_as_prompt({
promptForName=true,
}) {
return new Promise((resolve)=> {
const $w = new $FormToolWindow(dialogTitle);
const $w = new $DialogWindow(dialogTitle);
$w.addClass("save-as");
// @TODO: hotkeys (N, T, S, Enter, Esc)
@ -3183,7 +3197,7 @@ function sanity_check_blob(blob, okay_callback, magic_number_bytes, magic_wanted
if (magic_found === magic_wanted) {
okay_callback();
} else {
const $w = $FormToolWindow().title(localize("Paint")).addClass("dialogue-window");
const $w = $DialogWindow().title(localize("Paint")).addClass("dialogue-window");
// hackily combining messages that are already localized
// you have to do some deduction to understand this message
$w.$main.html(`
@ -3208,7 +3222,7 @@ function sanity_check_blob(blob, okay_callback, magic_number_bytes, magic_wanted
}
function show_multi_user_setup_dialog(from_current_document){
const $w = $FormToolWindow().title("Multi-User Setup").addClass("dialogue-window");
const $w = $DialogWindow().title("Multi-User Setup").addClass("dialogue-window");
$w.$main.html(`
${from_current_document ? "<p>This will make the current document public.</p>" : ""}
<p>

View File

@ -28,6 +28,7 @@ function open_help_viewer(options){
const $help_window = $Window({
title: options.title || "Help Topics",
icon: "chm",
resizable: true,
})
$help_window.addClass("help-window");
@ -70,7 +71,7 @@ function open_help_viewer(options){
$show_button.show();
$help_window.width($help_window.width() - toggling_width);
$help_window.css("left", $help_window.offset().left + toggling_width);
$help_window.bringTitleBarOnScreen();
$help_window.bringTitleBarInBounds();
});
const $show_button = add_toolbar_button("Show", 5, ()=> {
$contents.show();
@ -258,7 +259,7 @@ function open_help_viewer(options){
}, (/* error */)=> {
// access to error message is not allowed either, basically
if (location.protocol === "file:") {
const $w = $FormToolWindow().title(localize("Paint")).addClass("dialogue-window");
const $w = $DialogWindow().title(localize("Paint")).addClass("dialogue-window");
$w.$main.html(`
<p>${localize("Failed to launch help.")}</p>
<p>This feature is not available when running from the <code>file:</code> protocol.</p>

View File

@ -4,7 +4,7 @@ function show_imgur_uploader(blob){
if($imgur_window){
$imgur_window.close();
}
$imgur_window = $FormToolWindow().title("Upload To Imgur").addClass("dialogue-window");
$imgur_window = $DialogWindow().title("Upload To Imgur").addClass("dialogue-window");
const $preview_image_area = $(E("div")).appendTo($imgur_window.$main);//.html("<label style='display: block'>Preview:</label>");
const $imgur_url_area = $(E("div")).appendTo($imgur_window.$main);

View File

@ -11,7 +11,7 @@ function storage_quota_exceeded(){
if(ignoring_quota_exceeded){
return;
}
const $w = $FormToolWindow().title("Storage Error").addClass("dialogue-window squish");
const $w = $DialogWindow().title("Storage Error").addClass("dialogue-window squish");
$w.$main.html(
"<p>JS Paint stores images as you work on them so that if you " +
"close your browser or tab or reload the page " +
@ -38,7 +38,7 @@ function manage_storage(){
if($storage_manager){
$storage_manager.close();
}
$storage_manager = $FormToolWindow().title("Manage Storage").addClass("storage-manager dialogue-window squish");
$storage_manager = $DialogWindow().title("Manage Storage").addClass("storage-manager dialogue-window 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(

View File

@ -25,7 +25,7 @@
let $recovery_window;
function show_recovery_window(no_longer_blank) {
$recovery_window && $recovery_window.close();
const $w = $recovery_window = $FormToolWindow();
const $w = $recovery_window = $DialogWindow();
$w.on("close", ()=> {
$recovery_window = null;
});
@ -233,7 +233,7 @@
}
start() {
// @TODO: how do you actually detect if it's failing???
const $w = $FormToolWindow().title(localize("Paint")).addClass("dialogue-window");
const $w = $DialogWindow().title(localize("Paint")).addClass("dialogue-window");
$w.$main.html("<p>The document may not load. Changes may not save.</p>" +
"<p>Multiuser sessions are public. There is no security.</p>"
// "<p>The document may not load. Changes may not save. If it does save, it's public. There is no security.</p>"// +

View File

@ -604,7 +604,7 @@ html, body, .jspaint {
position: absolute;
z-index: 2;
}
.handle {
.canvas-area .handle {
position: absolute;
width: 3px;
height: 3px;

View File

@ -604,7 +604,7 @@ html, body, .jspaint {
position: absolute;
z-index: 2;
}
.handle {
.canvas-area .handle {
position: absolute;
width: 3px;
height: 3px;

View File

@ -19,7 +19,7 @@ html, body, .jspaint {
}
.window,
.handle,
.canvas-area .handle,
.status-area,
.menus,
.component-area,

View File

@ -48,7 +48,7 @@ body,
outline: 1px dashed black;
box-shadow: 0 0 0 1px white;
}
.handle {
.canvas-area .handle {
background: var(--Hilight);
}
.useless-handle {

View File

@ -43,7 +43,7 @@ body {
outline: 1px dashed black;
box-shadow: 0 0 0 1px white;
}
.handle {
.canvas-area .handle {
background: #000080;
}
.useless-handle {