Consolidate editor export buttons into a menu
This commit is contained in:
parent
c1bf88d3dd
commit
58cc6ff61e
@ -70,6 +70,9 @@
|
|||||||
<script>document.body.setAttribute('data-mode', 'loading');</script>
|
<script>document.body.setAttribute('data-mode', 'loading');</script>
|
||||||
<svg id="svg-iconsheet">
|
<svg id="svg-iconsheet">
|
||||||
<defs>
|
<defs>
|
||||||
|
<g id="svg-icon-menu-chevron">
|
||||||
|
<path d="M2,4 l6,6 l6,-6 v3 l-6,6 l-6,-6 z"></path>
|
||||||
|
</g>
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<g id="svg-icon-up">
|
<g id="svg-icon-up">
|
||||||
<path d="M0,12 l8,-8 l8,8 z"></path>
|
<path d="M0,12 l8,-8 l8,8 z"></path>
|
||||||
@ -95,11 +98,13 @@
|
|||||||
<path d="M 8,13 13,11 8,9 3,11 Z m 0,2 7,-3 V 11 L 8,8 1,11 v 1 z"></path>
|
<path d="M 8,13 13,11 8,9 3,11 Z m 0,2 7,-3 V 11 L 8,8 1,11 v 1 z"></path>
|
||||||
<ellipse cx="5.5" cy="11" rx="0.75" ry="0.5"></ellipse>
|
<ellipse cx="5.5" cy="11" rx="0.75" ry="0.5"></ellipse>
|
||||||
</g>
|
</g>
|
||||||
|
<!-- Hint background -->
|
||||||
<g id="svg-icon-hint">
|
<g id="svg-icon-hint">
|
||||||
<path d="M1,8 a7,7 0 1,1 14,0 7,7 0 1,1 -14,0 M2,8 a6,6 0 1,0 12,0 6,6 0 1,0 -12,0"></path>
|
<path d="M1,8 a7,7 0 1,1 14,0 7,7 0 1,1 -14,0 M2,8 a6,6 0 1,0 12,0 6,6 0 1,0 -12,0"></path>
|
||||||
<path d="M5,6 a1,1 0 0,0 2,0 a1,1 0 1,1 1,1 a1,1 0 0,0 -1,1 v1 a1,1 0 1,0 2,0 v-0.17 A3,3 0 1,0 5,6"></path>
|
<path d="M5,6 a1,1 0 0,0 2,0 a1,1 0 1,1 1,1 a1,1 0 0,0 -1,1 v1 a1,1 0 1,0 2,0 v-0.17 A3,3 0 1,0 5,6"></path>
|
||||||
<circle cx="8" cy="12" r="1"></circle>
|
<circle cx="8" cy="12" r="1"></circle>
|
||||||
</g>
|
</g>
|
||||||
|
<!-- Editor stuff -->
|
||||||
<g id="svg-icon-zoom">
|
<g id="svg-icon-zoom">
|
||||||
<path d="M1,6 a5,5 0 1,1 10,0 a5,5 0 1,1 -10,0 m2,0 a3,3 0 1,0 6,0 a3,3 0 1,0 -6,0"></path>
|
<path d="M1,6 a5,5 0 1,1 10,0 a5,5 0 1,1 -10,0 m2,0 a3,3 0 1,0 6,0 a3,3 0 1,0 -6,0"></path>
|
||||||
<path d="M14,12 l-2,2 -4,-4 2,-2 4,4"></path>
|
<path d="M14,12 l-2,2 -4,-4 2,-2 4,4"></path>
|
||||||
|
|||||||
@ -130,6 +130,11 @@ export class Overlay {
|
|||||||
// Use capture, which runs before any other event handler
|
// Use capture, which runs before any other event handler
|
||||||
window.addEventListener('keydown', this.keydown_handler, true);
|
window.addEventListener('keydown', this.keydown_handler, true);
|
||||||
|
|
||||||
|
// Block mouse movement as well
|
||||||
|
overlay.addEventListener('mousemove', ev => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +164,59 @@ export class TransientOverlay extends Overlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MenuOverlay extends TransientOverlay {
|
||||||
|
constructor(conductor, items, make_label, onclick) {
|
||||||
|
super(conductor, mk('ol.popup-menu'));
|
||||||
|
for (let [i, item] of items.entries()) {
|
||||||
|
this.root.append(mk('li', {'data-index': i}, make_label(item)));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.addEventListener('click', ev => {
|
||||||
|
let li = ev.target.closest('li');
|
||||||
|
if (! li || ! this.root.contains(li))
|
||||||
|
return;
|
||||||
|
|
||||||
|
let i = parseInt(li.getAttribute('data-index'), 10);
|
||||||
|
let item = items[i];
|
||||||
|
onclick(item);
|
||||||
|
this.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
open(relto) {
|
||||||
|
super.open();
|
||||||
|
|
||||||
|
let anchor = relto.getBoundingClientRect();
|
||||||
|
let rect = this.root.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Prefer left anchoring, but use right if that would go off the screen
|
||||||
|
if (anchor.left + rect.width > document.body.clientWidth) {
|
||||||
|
this.root.style.right = `${document.body.clientWidth - anchor.right}px`;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.root.style.left = `${anchor.left}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open vertically in whichever direction has more space (with a slight bias towards opening
|
||||||
|
// downwards). If we would then run off the screen, also set the other anchor to constrain
|
||||||
|
// the height.
|
||||||
|
let top_space = anchor.top - 0;
|
||||||
|
let bottom_space = document.body.clientHeight - anchor.bottom;
|
||||||
|
if (top_space > bottom_space) {
|
||||||
|
this.root.style.bottom = `${document.body.clientHeight - anchor.top}px`;
|
||||||
|
if (rect.height > top_space) {
|
||||||
|
this.root.style.top = `${0}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.root.style.top = `${anchor.bottom}px`;
|
||||||
|
if (rect.height > bottom_space) {
|
||||||
|
this.root.style.bottom = `${0}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Overlay styled like a dialog box
|
// Overlay styled like a dialog box
|
||||||
export class DialogOverlay extends Overlay {
|
export class DialogOverlay extends Overlay {
|
||||||
constructor(conductor) {
|
constructor(conductor) {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { TILES_WITH_PROPS } from './editor-tile-overlays.js';
|
|||||||
import * as format_base from './format-base.js';
|
import * as format_base from './format-base.js';
|
||||||
import * as c2g from './format-c2g.js';
|
import * as c2g from './format-c2g.js';
|
||||||
import * as dat from './format-dat.js';
|
import * as dat from './format-dat.js';
|
||||||
import { PrimaryView, TransientOverlay, DialogOverlay, AlertOverlay, flash_button, load_json_from_storage, save_json_to_storage } from './main-base.js';
|
import { PrimaryView, MenuOverlay, DialogOverlay, AlertOverlay, flash_button, load_json_from_storage, save_json_to_storage } from './main-base.js';
|
||||||
import CanvasRenderer from './renderer-canvas.js';
|
import CanvasRenderer from './renderer-canvas.js';
|
||||||
import TILE_TYPES from './tiletypes.js';
|
import TILE_TYPES from './tiletypes.js';
|
||||||
import { SVG_NS, mk, mk_button, mk_svg, string_from_buffer_ascii, bytestring_to_buffer, walk_grid } from './util.js';
|
import { SVG_NS, mk, mk_button, mk_svg, string_from_buffer_ascii, bytestring_to_buffer, walk_grid } from './util.js';
|
||||||
@ -3622,25 +3622,23 @@ export class Editor extends PrimaryView {
|
|||||||
this.save_level();
|
this.save_level();
|
||||||
});
|
});
|
||||||
|
|
||||||
_make_button("Download level as C2M", ev => {
|
let export_items = [
|
||||||
|
["Share this level with a link", () => {
|
||||||
|
// FIXME enable this once gliderbot can understand it
|
||||||
|
//let data = util.b64encode(fflate.zlibSync(new Uint8Array(c2g.synthesize_level(this.stored_level))));
|
||||||
|
let data = util.b64encode(c2g.synthesize_level(this.stored_level));
|
||||||
|
let url = new URL(location);
|
||||||
|
url.searchParams.delete('level');
|
||||||
|
url.searchParams.delete('setpath');
|
||||||
|
url.searchParams.append('level', data);
|
||||||
|
new EditorShareOverlay(this.conductor, url.toString()).open();
|
||||||
|
}],
|
||||||
|
["Download level as C2M", () => {
|
||||||
// TODO support getting warnings + errors out of synthesis
|
// TODO support getting warnings + errors out of synthesis
|
||||||
let buf = c2g.synthesize_level(this.stored_level);
|
let buf = c2g.synthesize_level(this.stored_level);
|
||||||
let blob = new Blob([buf]);
|
util.trigger_local_download((this.stored_level.title || 'untitled') + '.c2m', new Blob([buf]));
|
||||||
let url = URL.createObjectURL(blob);
|
}],
|
||||||
// To download a file, um, make an <a> and click it. Not kidding
|
["Download level as MSCC DAT/CCL", () => {
|
||||||
let a = mk('a', {
|
|
||||||
href: url,
|
|
||||||
download: (this.stored_level.title || 'untitled') + '.c2m',
|
|
||||||
});
|
|
||||||
document.body.append(a);
|
|
||||||
a.click();
|
|
||||||
// Absolutely no idea when I'm allowed to revoke this, but surely a minute is safe
|
|
||||||
window.setTimeout(() => {
|
|
||||||
a.remove();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}, 60 * 1000);
|
|
||||||
});
|
|
||||||
_make_button("Download level as MSCC DAT", ev => {
|
|
||||||
// TODO support getting warnings out of synthesis?
|
// TODO support getting warnings out of synthesis?
|
||||||
let buf;
|
let buf;
|
||||||
try {
|
try {
|
||||||
@ -3653,23 +3651,9 @@ export class Editor extends PrimaryView {
|
|||||||
}
|
}
|
||||||
throw errs;
|
throw errs;
|
||||||
}
|
}
|
||||||
|
util.trigger_local_download((this.stored_level.title || 'untitled') + '.ccl', new Blob([buf]));
|
||||||
let blob = new Blob([buf]);
|
}],
|
||||||
let url = URL.createObjectURL(blob);
|
["Download pack as C2G", () => {
|
||||||
// To download a file, um, make an <a> and click it. Not kidding
|
|
||||||
let a = mk('a', {
|
|
||||||
href: url,
|
|
||||||
download: (this.stored_level.title || 'untitled') + '.ccl',
|
|
||||||
});
|
|
||||||
document.body.append(a);
|
|
||||||
a.click();
|
|
||||||
// Absolutely no idea when I'm allowed to revoke this, but surely a minute is safe
|
|
||||||
window.setTimeout(() => {
|
|
||||||
a.remove();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}, 60 * 1000);
|
|
||||||
});
|
|
||||||
_make_button("Download pack", ev => {
|
|
||||||
let stored_pack = this.conductor.stored_game;
|
let stored_pack = this.conductor.stored_game;
|
||||||
|
|
||||||
// This is pretty heckin' best-effort for now; TODO move into format-c2g?
|
// This is pretty heckin' best-effort for now; TODO move into format-c2g?
|
||||||
@ -3712,33 +3696,23 @@ export class Editor extends PrimaryView {
|
|||||||
files[safe_title + '.c2g'] = fflate.strToU8(lines.join("\n"));
|
files[safe_title + '.c2g'] = fflate.strToU8(lines.join("\n"));
|
||||||
let u8array = fflate.zipSync(files);
|
let u8array = fflate.zipSync(files);
|
||||||
|
|
||||||
// TODO also allow download as CCL
|
|
||||||
// TODO support getting warnings + errors out of synthesis
|
// TODO support getting warnings + errors out of synthesis
|
||||||
let blob = new Blob([u8array]);
|
util.trigger_local_download((stored_pack.title || 'untitled') + '.zip', new Blob([u8array]));
|
||||||
let url = URL.createObjectURL(blob);
|
}],
|
||||||
// To download a file, um, make an <a> and click it. Not kidding
|
];
|
||||||
let a = mk('a', {
|
this.export_menu = new MenuOverlay(
|
||||||
href: url,
|
this.conductor,
|
||||||
download: (stored_pack.title || 'untitled') + '.zip',
|
export_items,
|
||||||
});
|
item => item[0],
|
||||||
document.body.append(a);
|
item => item[1](),
|
||||||
a.click();
|
);
|
||||||
// Absolutely no idea when I'm allowed to revoke this, but surely a minute is safe
|
let export_menu_button = _make_button("Export ", ev => {
|
||||||
window.setTimeout(() => {
|
this.export_menu.open(ev.currentTarget);
|
||||||
a.remove();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}, 60 * 1000);
|
|
||||||
});
|
|
||||||
_make_button("Share", ev => {
|
|
||||||
// FIXME enable this once gliderbot can understand it
|
|
||||||
//let data = util.b64encode(fflate.zlibSync(new Uint8Array(c2g.synthesize_level(this.stored_level))));
|
|
||||||
let data = util.b64encode(c2g.synthesize_level(this.stored_level));
|
|
||||||
let url = new URL(location);
|
|
||||||
url.searchParams.delete('level');
|
|
||||||
url.searchParams.delete('setpath');
|
|
||||||
url.searchParams.append('level', data);
|
|
||||||
new EditorShareOverlay(this.conductor, url.toString()).open();
|
|
||||||
});
|
});
|
||||||
|
export_menu_button.append(
|
||||||
|
mk_svg('svg.svg-icon', {viewBox: '0 0 16 16'},
|
||||||
|
mk_svg('use', {href: `#svg-icon-menu-chevron`})),
|
||||||
|
);
|
||||||
//_make_button("Toggle green objects");
|
//_make_button("Toggle green objects");
|
||||||
|
|
||||||
// Tile palette
|
// Tile palette
|
||||||
@ -4821,6 +4795,7 @@ export class Editor extends PrimaryView {
|
|||||||
overlay.edit_tile(tile, cell);
|
overlay.edit_tile(tile, cell);
|
||||||
overlay.open();
|
overlay.open();
|
||||||
|
|
||||||
|
// Fixed-size balloon positioning
|
||||||
// FIXME move this into TransientOverlay or some other base class
|
// FIXME move this into TransientOverlay or some other base class
|
||||||
let root = overlay.root;
|
let root = overlay.root;
|
||||||
let spacing = 2;
|
let spacing = 2;
|
||||||
|
|||||||
14
style.css
14
style.css
@ -240,6 +240,20 @@ svg.svg-icon {
|
|||||||
justify-content: start;
|
justify-content: start;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
.popup-menu {
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid black;
|
||||||
|
color: black;
|
||||||
|
background: hsl(225, 10%, 90%);
|
||||||
|
box-shadow: 0 1px 3px 1px #0009;
|
||||||
|
}
|
||||||
|
.popup-menu > li {
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.popup-menu > li:hover {
|
||||||
|
background: hsl(225, 40%, 75%);
|
||||||
|
}
|
||||||
.dialog {
|
.dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user