зеркало из https://github.com/MicrosoftEdge/Demos.git
329 строки
11 KiB
JavaScript
329 строки
11 KiB
JavaScript
import { STEPS } from './steps.js';
|
|
|
|
const flowList = document.querySelector('.flows ul');
|
|
const flowEditorName = document.querySelector('.editor .flow-name');
|
|
const flowEditorSteps = document.querySelector('.editor .steps');
|
|
const inputImages = document.querySelector('.input');
|
|
const outputImages = document.querySelector('.output');
|
|
const stepChooserDialog = document.querySelector('.step-chooser');
|
|
const stepChooserList = stepChooserDialog.querySelector('.steps');
|
|
const downloadImagesButton = document.querySelector('.download-images');
|
|
const saveImagesButton = document.querySelector('.save-images');
|
|
const useOutputAsInputButton = document.querySelector('.use-output-as-input');
|
|
const runFlowButton = document.querySelector('.run-flow');
|
|
const viewImagesButton = document.querySelector('.view-images');
|
|
|
|
// Editor UI
|
|
|
|
export function populateEditor(flow) {
|
|
flowEditorSteps.innerHTML = '';
|
|
flowEditorName.value = flow.name;
|
|
|
|
flow.steps.forEach((step, i) => {
|
|
flowEditorSteps.appendChild(createStep(step, i));
|
|
});
|
|
}
|
|
|
|
function createStep(step, index) {
|
|
const li = document.createElement('li');
|
|
li.dataset.type = step.type;
|
|
li.classList.add('step');
|
|
|
|
const icon = document.createElement('img');
|
|
icon.classList.add('step-icon');
|
|
icon.height = '40';
|
|
icon.src = `./icons/step-${step.type}.png`;
|
|
li.appendChild(icon);
|
|
|
|
const type = document.createElement('h3');
|
|
type.classList.add('step-type');
|
|
type.textContent = STEPS[step.type].name;
|
|
li.appendChild(type);
|
|
|
|
const description = document.createElement('p');
|
|
description.classList.add('step-description');
|
|
description.textContent = STEPS[step.type].description;
|
|
li.appendChild(description);
|
|
|
|
if (step.params && step.params.length) {
|
|
const params = document.createElement('div');
|
|
params.classList.add('step-params');
|
|
li.appendChild(params);
|
|
|
|
for (let i = 0; i < step.params.length; i++) {
|
|
const param = createParam(step.params[i], STEPS[step.type].params[i]);
|
|
params.appendChild(param);
|
|
}
|
|
}
|
|
|
|
const removeButton = document.createElement('button');
|
|
removeButton.dataset.index = index;
|
|
removeButton.classList.add('remove-step')
|
|
removeButton.classList.add('delete-icon');
|
|
removeButton.classList.add('no-text');
|
|
removeButton.textContent = 'Remove';
|
|
removeButton.setAttribute('title', 'Remove this step');
|
|
li.appendChild(removeButton);
|
|
|
|
return li;
|
|
}
|
|
|
|
export function insertStep(index) {
|
|
stepChooserDialog.showModal();
|
|
// For some reason the dialog's returnValue isn't reset when it's closed.
|
|
// So if the user opens the dialog again after having chosen a value before,
|
|
// even if they ESCape from it, the returnValue will still contain the
|
|
// previous value.
|
|
stepChooserDialog.returnValue = '';
|
|
|
|
return new Promise((resolve) => {
|
|
stepChooserDialog.addEventListener('close', (e) => {
|
|
const type = stepChooserDialog.returnValue;
|
|
if (!type) {
|
|
return resolve();
|
|
}
|
|
|
|
const params = STEPS[type].params && STEPS[type].params.length ? STEPS[type].params.map((param) => param.default) : null;
|
|
|
|
const li = createStep({ type, params });
|
|
|
|
flowEditorSteps.insertBefore(li, flowEditorSteps.querySelectorAll('.step')[index]);
|
|
|
|
resolve();
|
|
}, { once: true });
|
|
});
|
|
}
|
|
|
|
export function removeStep(index) {
|
|
flowEditorSteps.querySelectorAll('.step')[index].remove();
|
|
}
|
|
|
|
function createParam(param, paramDefinition) {
|
|
const paramEl = document.createElement('p');
|
|
paramEl.classList.add('step-param');
|
|
|
|
const paramName = document.createElement('label');
|
|
paramName.textContent = paramDefinition.name;
|
|
paramEl.appendChild(paramName);
|
|
|
|
let paramInput = null;
|
|
if (paramDefinition.type === 'select') {
|
|
paramInput = document.createElement('select');
|
|
paramDefinition.options.forEach((option) => {
|
|
const optionEl = document.createElement('option');
|
|
optionEl.value = option;
|
|
optionEl.textContent = option;
|
|
paramInput.appendChild(optionEl);
|
|
|
|
if (option === param) {
|
|
optionEl.selected = true;
|
|
}
|
|
});
|
|
} else {
|
|
paramInput = document.createElement('input');
|
|
paramInput.type = paramDefinition.type;
|
|
paramInput.value = param;
|
|
}
|
|
|
|
paramName.appendChild(paramInput);
|
|
|
|
return paramEl;
|
|
}
|
|
|
|
addEventListener('mousedown', mouseDownEvent => {
|
|
const movingStep = mouseDownEvent.target.closest('.editor .step');
|
|
const isInParam = mouseDownEvent.target.closest('.step-param');
|
|
const isInButton = mouseDownEvent.target.closest('.step .remove-step');
|
|
const onlyOneStep = flowEditorSteps.querySelectorAll('.step').length === 1;
|
|
|
|
if (!movingStep || isInParam || isInButton || onlyOneStep) {
|
|
return;
|
|
}
|
|
|
|
// Record the initial scroll position.
|
|
const initialScroll = flowEditorSteps.scrollTop;
|
|
|
|
// Mark the step as "moving" so it's taken out of the flow.
|
|
// And give it a real size since it will become absolutely positioned.
|
|
const mouseDelta = mouseDownEvent.clientY - movingStep.offsetTop + flowEditorSteps.scrollTop;
|
|
movingStep.style.top = `${movingStep.offsetTop - flowEditorSteps.scrollTop}px`;
|
|
movingStep.style.left = `${movingStep.offsetLeft}px`;
|
|
movingStep.style.width = `${movingStep.offsetWidth}px`;
|
|
movingStep.classList.add('moving');
|
|
|
|
// Create a placeholder with the same height x width.
|
|
// The placeholder is the one that will be moving, creating
|
|
// a visible gap in the list, where the moving step can be dropped.
|
|
const placeholder = document.createElement('li');
|
|
placeholder.classList.add('placeholder');
|
|
placeholder.style.height = `${movingStep.offsetHeight}px`;
|
|
placeholder.style.width = `${movingStep.offsetWidth}px`;
|
|
movingStep.parentNode.insertBefore(placeholder, movingStep);
|
|
|
|
const stepElements = [...flowEditorSteps.querySelectorAll('.step')];
|
|
|
|
// Restore the initial scroll position, because the DOM changes we just made
|
|
// have caused the scroll position to be reset.
|
|
flowEditorSteps.scrollTop = initialScroll;
|
|
|
|
function moveStep(mouseMoveEvent) {
|
|
movingStep.classList.toggle('started-moving', true);
|
|
movingStep.style.top = `${mouseMoveEvent.clientY - mouseDelta}px`;
|
|
|
|
// Check the position relative to other steps and move the placeholder.
|
|
for (const otherStep of stepElements) {
|
|
if (otherStep === movingStep) {
|
|
continue;
|
|
}
|
|
|
|
if (mouseMoveEvent.clientY > otherStep.offsetTop - flowEditorSteps.scrollTop &&
|
|
mouseMoveEvent.clientY < otherStep.offsetTop - flowEditorSteps.scrollTop + otherStep.offsetHeight / 2) {
|
|
otherStep.parentNode.insertBefore(placeholder, otherStep);
|
|
otherStep.parentNode.insertBefore(movingStep, otherStep);
|
|
} else if (mouseMoveEvent.clientY > otherStep.offsetTop - flowEditorSteps.scrollTop + otherStep.offsetHeight / 2 &&
|
|
mouseMoveEvent.clientY < otherStep.offsetTop - flowEditorSteps.scrollTop + otherStep.offsetHeight) {
|
|
otherStep.parentNode.insertBefore(placeholder, otherStep.nextSibling);
|
|
otherStep.parentNode.insertBefore(movingStep, otherStep.nextSibling);
|
|
}
|
|
}
|
|
}
|
|
|
|
addEventListener('mousemove', moveStep);
|
|
addEventListener('mouseup', () => {
|
|
movingStep.classList.remove('moving');
|
|
movingStep.classList.remove('started-moving');
|
|
removeEventListener('mousemove', moveStep);
|
|
|
|
placeholder.remove();
|
|
|
|
// Let the app know that something changed by firing a flow-change event.
|
|
dispatchEvent(new Event('flow-change'));
|
|
}, { once: true });
|
|
});
|
|
|
|
// List of flows
|
|
|
|
export function populateFlowList(flows, selectId) {
|
|
flowList.innerHTML = '';
|
|
|
|
flows.forEach((flow) => {
|
|
flowList.appendChild(createFlowListEntry(flow, selectId));
|
|
});
|
|
}
|
|
|
|
function createFlowListEntry(flow, selectId) {
|
|
const li = document.createElement('li');
|
|
|
|
li.classList.add('flow-in-list');
|
|
li.setAttribute('title', 'Open flow');
|
|
if (selectId && flow.id === selectId) {
|
|
li.classList.add('selected');
|
|
}
|
|
li.dataset.id = flow.id;
|
|
|
|
const a = document.createElement('a');
|
|
a.href = `#`;
|
|
a.dataset.id = flow.id;
|
|
li.appendChild(a);
|
|
|
|
const name = document.createElement('span');
|
|
name.classList.add('flow-name');
|
|
name.textContent = flow.name;
|
|
a.appendChild(name);
|
|
|
|
const nbOfSteps = document.createElement('span');
|
|
nbOfSteps.classList.add('flow-nb-of-steps');
|
|
nbOfSteps.textContent =
|
|
`${flow.steps.length} step${flow.steps.length > 1 ? 's' : ''}: ${flow.steps.map((step) => STEPS[step.type].name.toLowerCase()).join(', ')}`;
|
|
a.appendChild(nbOfSteps);
|
|
|
|
return li;
|
|
}
|
|
|
|
// Populate the step chooser dialog, right from the start.
|
|
|
|
function populateStepChooserDialog() {
|
|
for (const key of Object.keys(STEPS)) {
|
|
const button = document.createElement('button');
|
|
button.setAttribute('type', 'submit');
|
|
button.setAttribute('value', key);
|
|
button.classList.add('step-to-choose');
|
|
|
|
const icon = document.createElement('img');
|
|
icon.src = `./icons/step-${key}.png`;
|
|
icon.width = 40;
|
|
button.appendChild(icon);
|
|
|
|
const type = document.createElement('h3');
|
|
type.classList.add('step-type');
|
|
type.textContent = STEPS[key].name;
|
|
button.appendChild(type);
|
|
|
|
const description = document.createElement('p');
|
|
description.classList.add('step-description');
|
|
description.textContent = STEPS[key].description;
|
|
button.appendChild(description);
|
|
|
|
stepChooserList.appendChild(button);
|
|
}
|
|
}
|
|
|
|
populateStepChooserDialog();
|
|
|
|
// List of images.
|
|
|
|
export function populateInputImages(images) {
|
|
populateImages(images, inputImages);
|
|
|
|
runFlowButton.disabled = images.length === 0;
|
|
}
|
|
|
|
export function populateOutputImages(images, supportsFSHandleSave) {
|
|
populateImages(images, outputImages);
|
|
|
|
downloadImagesButton.toggleAttribute('disabled', images.length === 0);
|
|
useOutputAsInputButton.toggleAttribute('disabled', images.length === 0);
|
|
saveImagesButton.toggleAttribute('disabled', !supportsFSHandleSave || images.length === 0);
|
|
viewImagesButton.toggleAttribute('disabled', images.length === 0);
|
|
|
|
outputImages.parentNode.classList.toggle('has-output-images', images.length > 0);
|
|
}
|
|
|
|
function populateImages(images, container) {
|
|
container.classList.toggle('empty', images.length === 0);
|
|
|
|
// Remove all elements from the container except the instructions.
|
|
[...container.children].forEach((child) => {
|
|
if (!child.classList.contains('instructions')) {
|
|
child.remove();
|
|
}
|
|
});
|
|
|
|
// Sort the images by name so input and output are in the same order.
|
|
images.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
for (const { src, name } of images) {
|
|
const div = document.createElement('div');
|
|
div.classList.add('image');
|
|
|
|
const img = document.createElement('img');
|
|
img.src = src;
|
|
div.appendChild(img);
|
|
|
|
const nameP = document.createElement('p');
|
|
nameP.classList.add('image-name');
|
|
nameP.textContent = name;
|
|
div.appendChild(nameP);
|
|
|
|
const sizeP = document.createElement('p');
|
|
sizeP.classList.add('image-size');
|
|
div.appendChild(sizeP);
|
|
|
|
img.onload = () => {
|
|
sizeP.textContent = `${img.naturalWidth} x ${img.naturalHeight}`;
|
|
}
|
|
|
|
container.appendChild(div);
|
|
}
|
|
}
|