Add drag+drop with interact.js (closes #2)
This commit is contained in:
parent
4e846ca773
commit
eb6aae2ba0
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
})();
|
72
index.lua
72
index.lua
|
@ -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
|
||||
|
|
46
renderer.lua
46
renderer.lua
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue