Add drag+drop with interact.js (closes #2)

This commit is contained in:
luk3yx 2021-02-27 11:42:19 +13:00
parent 4e846ca773
commit eb6aae2ba0
6 changed files with 226 additions and 27 deletions

View File

@ -16,6 +16,7 @@ default (dynamically loaded when required).
## Major features
- Web-based (no waiting for MT to load)
- Dragghing and resizing elements.
- Property editor
- `${lua code}` substitution in text values.
- Don't remove the weird comments generated when exporting these formspecs
@ -33,7 +34,8 @@ default (dynamically loaded when required).
- Malicious formspecs imported with the `${...}` substitution option enabled
can freeze the webpage.
- Element alignment might not be perfect.
- There may be bugs in Google Chrome, I have only tested this in Firefox.
- I haven't tested this thoroughly in many browsers, if you find any bugs
please report them.
- Texture modifiers in `image[]` will not be displayed in the preview.
## Copyright / License

View File

@ -48,7 +48,7 @@ local function export(tree, backport_func)
node.selected_id, node.selected_idx = node.selected_idx, nil
node.choices, node.item = node.item, nil
-- Later versions of the digustuff mod require a height field even
-- for formspec version 1 (breaking dropdowns on older clients).
-- for formspec version 1.
node.h = node.h or 0.81
elseif node.type == 'image_button' or
node.type == 'image_button_exit' then

View File

@ -3,8 +3,10 @@
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<link rel="stylesheet" type="text/css" href="style.css" />
<script src="https://unpkg.com/fengari-web/dist/fengari-web.js"></script>
<script type="application/lua" src="index.lua?rev=4"></script>
<script src="https://unpkg.com/fengari-web/dist/fengari-web.js" async></script>
<script src="https://unpkg.com/interactjs/dist/interact.min.js" async></script>
<script src="index.js" async></script>
<script type="application/lua" src="index.lua?rev=5" async defer></script>
<style>
body {
background: #8CBAFA;

123
index.js Normal file
View File

@ -0,0 +1,123 @@
"use strict";
(() => {
window.addEventListener("beforeunload", e => {
return (e || window.event).returnValue = "Are you sure you want to go?";
});
if (window.location.search == "?no-drag-drop")
return;
window.basic_interact = {};
let snap;
basic_interact.snap = () => {
if (!snap)
snap = interact.modifiers.snap({
targets: [
interact.snappers.grid({x: 5, y: 5}),
],
range: Infinity,
offset: 'self',
relativePoints: [{x: 0, y: 0}]
})
return snap;
};
basic_interact.add = (target, draggable, resizable, callback) => {
let x = 0;
let y = 0;
target.style.touchAction = "none";
if (target.style.userSelect)
target.setAttribute("data-old-user-select", target.style.userSelect);
target.style.userSelect = "none";
target.classList.add("drag_drop");
if (resizable)
target.style.boxSizing = "border-box";
function move() {
target.style.transform = `translate(${x}px, ${y}px)`;
};
move();
function endMove() {
callback.call(target, x, y, target.offsetWidth, target.offsetHeight);
};
const interact_target = interact(target).on("tap", () => {
callback.call(target);
});
if (draggable)
interact_target.draggable({
// inertia: true,
listeners: {
end: endMove,
move (event) {
x += event.dx;
y += event.dy;
move();
},
},
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true,
}),
basic_interact.snap()
],
});
if (resizable)
interact_target.resizable({
edges: {
top: true,
left: true,
bottom: true,
right: true,
},
listeners: {
end: endMove,
move (event) {
target.style.width = `${event.rect.width}px`;
target.style.height = `${event.rect.height}px`;
x += event.deltaRect.left;
y += event.deltaRect.top;
move();
}
},
modifiers: [
// interact.modifiers.restrictRect({
// restriction: 'parent',
// endOnly: true,
// }),
basic_interact.snap()
],
invert: "reposition",
});
};
basic_interact.remove = target => {
interact(target).unset();
target.style.transform = "";
target.style.touchAction = "";
const old_user_select = target.getAttribute("data-old-user-select");
target.style.userSelect = old_user_select || "";
target.removeAttribute("data-old-user-select");
target.classList.remove("drag_drop");
delete basic_interact.target;
};
window.addEventListener("load", () => {
const elem = document.createElement("style");
elem.innerHTML = `
.drag_drop * {
cursor: inherit;
}
`;
document.head.appendChild(elem);
});
})();

View File

@ -18,7 +18,7 @@
--
-- Load the renderer
dofile('renderer.lua?rev=4')
dofile('renderer.lua?rev=5')
local formspec_escape = formspec_ast.formspec_escape
local _, digistuff_ts_export = dofile('digistuff_ts.lua?rev=4')
@ -117,6 +117,11 @@ local function draw_elements_list(selected_element)
return formspec .. ';' .. selected .. ']'
end
local SCALE = 50
local function round_pos(pos)
return math.floor(pos * 10 + 0.5) / 10
end
local function show_properties(elem, node)
if not properties_elem then
properties_elem = document:createElement('div')
@ -345,24 +350,61 @@ local function show_properties(elem, node)
properties_elem:appendChild(n)
end
renderer.default_callback = show_properties
-- Set up drag+drop. This is mostly done in JavaScript for performance.
function renderer.default_elem_hook(node, elem)
local basic_interact = js.global.basic_interact
if not basic_interact then return show_properties end
local draggable = node.x ~= nil and node.y ~= nil
local resizable = node.w ~= nil and node.h ~= nil and node.type ~= "list"
local orig_x, orig_y = node.x, node.y
basic_interact:add(elem, draggable, resizable, function(_, x, y, w, h)
local modified
if draggable and x then
node.x = round_pos(orig_x + x / SCALE)
node.y = round_pos(orig_y + y / SCALE)
modified = true
end
if resizable and w then
node.w = round_pos(math.max(w / SCALE, 0.1))
node.h = round_pos(math.max(h / SCALE, 0.1))
modified = true
end
if modified then
elem:setAttribute('data-formspec_ast', json.dumps(node))
local idx = window.Array.prototype.indexOf(
elem.parentNode.children, elem)
local base = renderer.redraw_formspec(elem.parentNode.parentNode)
if idx >= 0 then
show_properties(base.firstChild.children[idx])
end
else
show_properties(elem)
end
end)
return true
end
-- Templates for new elements
do
local templates = assert(formspec_ast.parse([[
size[10.5,11]
box[0,0;1,1;]
button[0,0;3,0.75;;]
button_exit[0,0;3,0.75;;]
button[0,0;3,0.8;;]
button_exit[0,0;3,0.8;;]
checkbox[0,0.2;;;false]
dropdown[0,0;3,0.75;;;1]
field[0,0;3,0.75;;;]
dropdown[0,0;3,0.8;;;1]
field[0,0;3,0.8;;;]
image[0,0;1,1;]
image_button[0,0;2,2;;;;false;true;]
image_button_exit[0,0;2,2;;;]
label[0,0.2;]
list[current_player;main;0,0;8,4;0]
pwdfield[0,0;3,0.75;;]
pwdfield[0,0;3,0.8;;]
textarea[0,0;3,2;;;]
textlist[0,0;5,3;;;1;false]
]]))
@ -543,7 +585,8 @@ function renderer.show_element_dialog(base)
end
y = y + 0.5
fs = fs .. 'button[0.25,' .. y .. ';5.5,0.75;grid;Toggle grid]'
fs = fs .. 'button[0.25,' .. y + 1 .. ';5.5,0.75;load;Load / save formspec]'
y = y + 1
fs = fs .. 'button[0.25,' .. y .. ';5.5,0.75;load;Load / save formspec]'
function callbacks.grid()
local raw = element_dialog_base:getAttribute('data-render-options')
if raw == js.null then raw = '{}' end
@ -557,8 +600,17 @@ function renderer.show_element_dialog(base)
end
end
callbacks.load = show_load_save_dialog
y = y + 2
fs = 'formspec_version[2]size[6,' .. y .. ']' .. fs
if js.global.basic_interact then
y = y + 1
fs = fs .. 'button[0.25,' .. y ..
';5.5,0.75;drag_drop;Disable drag+drop]'
function callbacks.drag_drop()
window.location.search = '?no-drag-drop'
end
end
fs = 'formspec_version[3]size[6,' .. y + 1 .. ']' .. fs
element_dialog:appendChild(assert(renderer.render_formspec(fs, callbacks,
{store_json = false})))
end

View File

@ -35,6 +35,8 @@ document = window.document
renderer = {}
local type = type
-- Render formspecs to HTML
local elems = {}
@ -241,41 +243,57 @@ function elems.dropdown(node, base, default_callbacks, scale)
return res
end
local invisible_nodes = {style = true, position = true, anchor = true}
local warned = {}
local function generic_render(node)
window.console:warn('Formspec element type ' .. node.type ..
' not implemented.')
if node.x and node.y then
return renderer.make_image('unknown_object.png')
else
local visible = not invisible_nodes[node.type]
if visible then
if not warned[node.type] then
warned[node.type] = true
window.console:warn('Formspec element type ' .. node.type ..
' not implemented.')
end
if node.x and node.y then
return renderer.make_image('unknown_object.png')
end
window.console:error('Formspec element type ' .. node.type ..
' is not implemented and there is no reliable way to render it.')
local res = make('div')
res.style.display = 'none'
return res
' is not implemented and there is no reliable way to' ..
' render it.')
end
local res = make('div')
res.style.display = 'none'
return res
end
-- Make images - This uses HDX to simplify things
local image_baseurl = 'https://gitlab.com/VanessaE/hdx-128/raw/master/'
local mode_cache = {}
function renderer.make_image(name, allow_empty)
-- Remove extension
local real_name = name:match('^(.*)%.[^%.]+$') or ''
-- Make an <img> element
local img = document:createElement('img')
local mode = 'png'
local mode = mode_cache[name] or 'png'
img:setAttribute('ondragstart', 'return false')
if name == '' and allow_empty then
img.style.opacity = '0'
return img
elseif name == '' or mode == '' then
img.src = image_baseurl .. 'unknown_node.png'
return img
end
img:addEventListener('error', function()
if mode == 'png' then
mode = 'jpg'
mode_cache[name] = 'jpg'
elseif mode == nil then
return
else
mode = nil
mode_cache[name] = ''
img.src = image_baseurl .. 'unknown_node.png'
return
end
@ -330,11 +348,13 @@ function renderer.render_ast(tree, callbacks, options)
if type(callbacks) == 'table' then
func = callbacks[node.name or '']
elseif callbacks == nil then
func = renderer.default_callback
func = renderer.default_elem_hook(node, e)
end
if func then
e:addEventListener('click', func)
e.className = e.className .. ' formspec_ast-clickable'
if type(func) == 'function' then
e:addEventListener('click', func)
end
e.classList:add('formspec_ast-clickable')
end
container:appendChild(e)
end