You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte/sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte

463 lines
10 KiB

<script>
import { get_repl_context } from '$lib/context.js';
import { get_full_filename } from '$lib/utils.js';
import { createEventDispatcher, tick } from 'svelte';
import RunesInfo from './RunesInfo.svelte';
import Migrate from './Migrate.svelte';
/** @type {boolean} */
export let show_modified;
/** @type {boolean} */
export let runes;
/** @type {ReturnType<typeof createEventDispatcher<{
* remove: { files: import('$lib/types').File[]; diff: import('$lib/types').File },
* add: { files: import('$lib/types').File[]; diff: import('$lib/types').File },
* }>>} */
const dispatch = createEventDispatcher();
const {
files,
handle_select,
module_editor,
rebundle,
selected,
selected_name,
EDITOR_STATE_MAP
} = get_repl_context();
/** @type {string | null} */
let editing_name = null;
let input_value = '';
/** @param {string} filename */
function select_file(filename) {
if ($selected_name !== filename) {
editing_name = null;
handle_select(filename);
}
}
/** @param {import('$lib/types').File} file */
function edit_tab(file) {
if ($selected_name === get_full_filename(file)) {
editing_name = get_full_filename(file);
input_value = file.name;
}
}
async function close_edit() {
const match = /(.+)\.(svelte|js|json|md|css)$/.exec(input_value ?? '');
const edited_file = $files.find((val) => get_full_filename(val) === editing_name);
if (!edited_file) return;
edited_file.name = match ? match[1] : input_value;
if (!$selected) return;
if (is_file_name_used($selected)) {
let i = 1;
let name = $selected.name;
do {
const file = $files.find(
(val) =>
get_full_filename(val) === get_full_filename(edited_file) &&
// @ts-ignore
val.source === $selected.source
);
if (!file) break;
file.name = `${name}_${i++}`;
} while (is_file_name_used($selected));
const idx = $files.findIndex(
(val) => get_full_filename(val) === get_full_filename(edited_file)
);
$files[idx] = edited_file;
}
const idx = $files.findIndex(
(val) => get_full_filename(val) === get_full_filename(edited_file)
);
if (match?.[2]) $files[idx].type = match[2];
if (editing_name) {
const old_state = EDITOR_STATE_MAP.get(editing_name);
if (old_state) {
EDITOR_STATE_MAP.set(get_full_filename(edited_file), old_state);
EDITOR_STATE_MAP.delete(editing_name);
}
}
editing_name = null;
// re-select, in case the type changed
handle_select(get_full_filename(edited_file));
$files = $files;
// focus the editor, but wait a beat (so key events aren't misdirected)
await tick();
$module_editor?.focus();
rebundle();
}
/**
* @param {string} filename
*/
function remove(filename) {
const file = $files.find((val) => get_full_filename(val) === filename);
const idx = $files.findIndex((val) => get_full_filename(val) === filename);
if (!file) return;
let result = confirm(`Are you sure you want to delete ${get_full_filename(file)}?`);
if (!result) return;
$files = $files.filter((file) => get_full_filename(file) !== filename);
dispatch('remove', { files: $files, diff: file });
EDITOR_STATE_MAP.delete(get_full_filename(file));
$selected_name = idx === 1 ? 'App.svelte' : get_full_filename(file);
handle_select($selected_name);
}
/** @param {FocusEvent & { currentTarget: HTMLInputElement }} event */
async function select_input(event) {
await tick();
event.currentTarget.select();
}
let uid = 1;
function add_new() {
const file = {
name: uid++ ? `Component${uid}` : 'Component1',
type: 'svelte',
source: '',
modified: true
};
$files = $files.concat(file);
editing_name = get_full_filename(file);
input_value = file.name;
handle_select(editing_name);
rebundle();
dispatch('add', { files: $files, diff: file });
$files = $files;
}
/** @param {import('$lib/types').File} editing */
function is_file_name_used(editing) {
return $files.find(
(file) => JSON.stringify(file) !== JSON.stringify($selected) && file.name === editing.name
);
}
// drag and drop
/** @type {string | null} */
let from = null;
/** @type {string | null} */
let over = null;
/** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
function dragStart(event) {
from = event.currentTarget.id;
}
function dragLeave() {
over = null;
}
/** @param {DragEvent & { currentTarget: HTMLDivElement }} event */
function dragOver(event) {
over = event.currentTarget.id;
}
function dragEnd() {
if (from && over) {
const from_index = $files.findIndex((file) => file.name === from);
const to_index = $files.findIndex((file) => file.name === over);
const from_component = $files[from_index];
$files.splice(from_index, 1);
$files = $files.slice(0, to_index).concat(from_component).concat($files.slice(to_index));
}
from = over = null;
}
</script>
<div class="component-selector">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="file-tabs" on:dblclick={add_new}>
{#each $files as file, index (file.name)}
{@const filename = get_full_filename(file)}
<div
id={file.name}
class="button"
role="button"
tabindex="0"
class:active={filename === $selected_name}
class:draggable={filename !== editing_name && index !== 0}
class:drag-over={over === file.name}
on:click={() => select_file(filename)}
on:keyup={(e) => e.key === ' ' && select_file(filename)}
on:dblclick|stopPropagation={() => {}}
draggable={filename !== editing_name}
on:dragstart={dragStart}
on:dragover|preventDefault={dragOver}
on:dragleave={dragLeave}
on:drop|preventDefault={dragEnd}
>
<i class="drag-handle" />
{#if file.name === 'App' && filename !== editing_name}
<div class="uneditable">
App.svelte{#if show_modified && file.modified}*{/if}
</div>
{:else if filename === editing_name}
{@const editing_file = $files.find((file) => get_full_filename(file) === editing_name)}
{#if editing_file}
<span class="input-sizer">
{input_value + (/\./.test(input_value) ? '' : `.${editing_file.type}`)}
</span>
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
spellcheck={false}
bind:value={input_value}
on:focus={select_input}
on:blur={close_edit}
on:keydown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
if (!is_file_name_used(editing_file)) {
e.currentTarget.blur();
}
}
}}
class:duplicate={is_file_name_used(editing_file)}
/>
{/if}
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="editable"
title="edit component name"
on:click={() => edit_tab(file)}
on:keyup={(e) => e.key === ' ' && edit_tab(file)}
>
{file.name}.{file.type}{#if show_modified && file.modified}*{/if}
</div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span
class="remove"
on:click={() => remove(filename)}
on:keyup={(e) => e.key === ' ' && remove(filename)}
>
<svg width="12" height="12" viewBox="0 0 24 24">
<line stroke="#999" x1="18" y1="6" x2="6" y2="18" />
<line stroke="#999" x1="6" y1="6" x2="18" y2="18" />
</svg>
</span>
{/if}
</div>
{/each}
</div>
<button class="add-new" on:click={add_new} title="add new component">
<svg width="12" height="12" viewBox="0 0 24 24">
<line stroke="#999" x1="12" y1="5" x2="12" y2="19" />
<line stroke="#999" x1="5" y1="12" x2="19" y2="12" />
</svg>
</button>
<div class="runes-info"><RunesInfo {runes} /></div>
<div class="migrate-info"><Migrate /></div>
</div>
<style>
.component-selector {
position: relative;
border-bottom: 1px solid var(--sk-text-4);
/* overflow: hidden; */
display: flex;
}
.file-tabs {
border: none;
margin: 0;
white-space: nowrap;
overflow-x: auto;
overflow-y: hidden;
}
.file-tabs .button,
.add-new {
position: relative;
display: inline-block;
font: 400 12px/1.5 var(--sk-font);
background: var(--sk-back-1);
border: none;
border-bottom: 3px solid transparent;
padding: 12px 14px 8px 16px;
margin: 0;
color: var(--sk-text-3);
border-radius: 0;
cursor: pointer;
}
.file-tabs .button.active {
/* color: var(--second); */
color: var(--sk-text-2, #333);
border-bottom: 3px solid var(--sk-theme-1);
}
.editable,
.uneditable,
.input-sizer,
input {
display: inline-block;
position: relative;
line-height: 1;
}
.input-sizer {
color: var(--sk-text-3, #ccc);
}
input {
position: absolute;
width: 100%;
left: 16px;
top: 12px;
font: 400 12px/1.5 var(--sk-font);
border: none;
color: var(--sk-theme-3);
outline: none;
background-color: transparent;
}
.duplicate {
color: var(--sk-theme-1);
}
.remove {
position: absolute;
display: none;
right: 1px;
top: 4px;
width: 16px;
text-align: right;
padding: 12px 0 12px 5px;
font-size: 8px;
cursor: pointer;
}
.remove:hover {
color: var(--sk-theme-3);
}
.file-tabs .button.active .editable {
cursor: text;
}
.file-tabs .button.active .remove {
display: block;
}
.file-tabs .button.drag-over {
background: #67677814;
}
.file-tabs .button.drag-over {
cursor: move;
}
.add-new {
padding: 12px 10px 8px 8px;
height: 40px;
text-align: center;
}
.add-new:hover {
color: var(--sk-theme-3) !important;
}
.runes-info {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
}
.migrate-info {
flex: 0 1 0;
display: flex;
align-items: center;
justify-content: flex-end;
}
.drag-handle {
cursor: move;
width: 5px;
height: 25px;
position: absolute;
left: 5px;
top: 9px;
--drag-handle-color: #dedede;
background: linear-gradient(
to right,
var(--sk-back-4, --drag-handle-color) 1px,
var(--sk-back-1, white) 1px,
var(--sk-back-1, white) 2px,
var(--sk-back-4, --drag-handle-color) 2px,
var(--sk-back-4, --drag-handle-color) 3px,
var(--sk-back-1, white) 3px,
var(--sk-back-1, white) 4px,
var(--sk-back-4, --drag-handle-color) 4px
);
}
svg {
position: relative;
overflow: hidden;
vertical-align: middle;
-o-object-fit: contain;
object-fit: contain;
-webkit-transform-origin: center center;
transform-origin: center center;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
</style>