Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion formwork/src/Assets/Asset.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ class Asset
private string $mimeType;

/**
* @param array<string, mixed> $meta Asset metadata
*
* @throws AssetNotFoundException If the asset file is not found
*/
public function __construct(string $path, string $uri)
public function __construct(string $path, string $uri, private array $meta = [])
{
$this->path = FileSystem::normalizePath($path);
$this->uri = Uri::normalize($uri);
Expand Down Expand Up @@ -105,4 +107,12 @@ public function toBase64(): string
{
return 'data:' . $this->mimeType() . ';base64,' . base64_encode($this->content());
}

/**
* Get asset metadata value by key
*/
public function getMeta(string $key, mixed $default = null): mixed
{
return $this->meta[$key] ?? $default;
}
}
14 changes: 12 additions & 2 deletions formwork/src/Assets/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,26 @@ public function __construct(string $basePath, string $baseUri)

/**
* Add an asset to the collection
*
* @param array<string, mixed> $meta Asset metadata
*/
public function add(string $key): void
public function add(string $key, array $meta = []): void
{
if (!$this->collection->has($key)) {
$path = FileSystem::joinPaths($this->basePath, Path::resolve($key, '/', DIRECTORY_SEPARATOR));
$uri = Path::join([$this->baseUri, Path::resolve($key, '/')]);
$this->collection->set($key, new Asset($path, $uri));
$this->collection->set($key, new Asset($path, $uri, $meta));
}
}

/**
* Return whether the collection has an asset with the given key
*/
public function has(string $key): bool
{
return $this->collection->has($key);
}

/**
* Get an asset from the collection
*/
Expand Down
24 changes: 24 additions & 0 deletions panel/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as esbuild from "esbuild";
import process from "process";

const watch = process.argv.includes("--watch");

const options = {
entryPoints: { "app.min": "./src/ts/app.ts" },
bundle: true,
format: "esm",
target: "es2020",
chunkNames: "chunks/[name]-[hash]",
minify: true,
splitting: true,
outdir: "./assets/js",
logLevel: "info",
};

if (watch) {
// New esbuild API for watch mode
const ctx = await esbuild.context(options);
await ctx.watch();
Comment thread
giuscris marked this conversation as resolved.
} else {
await esbuild.build(options);
}
23 changes: 19 additions & 4 deletions panel/eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { defineConfig } from "eslint/config";
Comment thread
giuscris marked this conversation as resolved.
Comment thread
giuscris marked this conversation as resolved.
import eslintConfigPrettier from "eslint-config-prettier";
import globals from "globals";
import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default [
export default defineConfig([
js.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
ecmaVersion: 13,
ecmaVersion: 2020,
globals: {
...globals.browser,
Formwork: "readonly",
},
sourceType: "module",
},
Expand All @@ -36,14 +36,20 @@ export default [
"prefer-arrow-callback": ["error"],
"prefer-const": ["error"],
"prefer-template": ["error"],
"require-await": ["error"],
"sort-imports": [
"warn",
{
ignoreCase: true,
allowSeparatedGroups: true,
},
],
"@typescript-eslint/consistent-type-exports": ["error"],
"@typescript-eslint/consistent-type-imports": ["error"],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-redundant-type-constituents": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/typedef": [
"warn",
{
Expand All @@ -52,5 +58,14 @@ export default [
],
},
},
{
files: ["**/*.ts"],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
eslintConfigPrettier,
];
]);
5 changes: 3 additions & 2 deletions panel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
"directory": "panel"
},
"scripts": {
"build": "pnpm build:css && pnpm build:js",
"build": "pnpm clean && pnpm build:css && pnpm build:js",
"build:css": "sass ./src/scss/panel.scss:./assets/css/panel.min.css --style=compressed --no-source-map",
"build:js": "tsc && esbuild ./src/ts/app.ts --outfile=./assets/js/app.min.js --bundle --format=iife --global-name=Formwork --target=es6 --minify",
"build:js": "tsc && node build.js",
"clean": "rm -rf ./assets/js/chunks/**/*.js",
Comment thread
giuscris marked this conversation as resolved.
"watch": "pnpm watch:css & pnpm watch:js",
"watch:css": "pnpm build:css --watch",
"watch:js": "pnpm build:js --watch",
Expand Down
2 changes: 1 addition & 1 deletion panel/src/ts/components/dropdowns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class Dropdowns {

if (button) {
const dropdown = document.getElementById(button.dataset.dropdown as string) as HTMLElement;
const isVisible = getComputedStyle(dropdown as HTMLElement).display !== "none";
const isVisible = getComputedStyle(dropdown).display !== "none";
event.preventDefault();

const resizeHandler = throttle(() => setDropdownPosition(dropdown), 100);
Expand Down
2 changes: 1 addition & 1 deletion panel/src/ts/components/fileslist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { $, $$ } from "../utils/selectors";
import { escapeHtml, escapeRegExp, makeDiacriticsRegExp } from "../utils/validation";
import { app } from "../app";
import { debounce } from "../utils/events";
import { Form } from "./form";
import type { Form } from "./form";
import { Notification } from "./notification";
import { Request } from "../utils/request";
import { SelectInput } from "./inputs/select-input";
Expand Down
11 changes: 8 additions & 3 deletions panel/src/ts/components/inputs/array-input.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { $, $$ } from "../../utils/selectors";
import { Form, HTMLInputLike } from "../form";
import Sortable from "sortablejs";
import type { Form, HTMLInputLike } from "../form";

export class ArrayInput {
readonly element: HTMLFieldSetElement;
Expand All @@ -16,6 +15,10 @@ export class ArrayInput {

this.isAssociative = element.classList.contains("form-input-array-associative");

this.init(element);
}

private async init(element: HTMLFieldSetElement) {
$$(".form-input-array-row", element).forEach((element) => this.bindItemEvents(element));

$(`label[for="${element.id}"]`)?.addEventListener("click", () => $(".form-input", element)?.focus());
Expand All @@ -24,6 +27,8 @@ export class ArrayInput {
this.form.element.addEventListener("submit", () => this.handleSubmit());
}

const { default: Sortable } = await import("sortablejs");

Sortable.create(element, {
handle: ".sortable-handle",
forceFallback: true,
Expand All @@ -34,7 +39,7 @@ export class ArrayInput {
}

get name(): string {
return this.element.name as string;
return this.element.name;
}

set name(value: string) {
Expand Down
2 changes: 1 addition & 1 deletion panel/src/ts/components/inputs/duration-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class DurationInput {
Object.keys(TIME_INTERVALS).forEach((t: TimeInterval) => {
if (intervalNames.includes(t)) {
intervals[t] = Math.floor(seconds / TIME_INTERVALS[t]);
seconds -= (intervals[t] as number) * TIME_INTERVALS[t];
seconds -= (intervals[t]) * TIME_INTERVALS[t];
}
});
return intervals;
Expand Down
44 changes: 29 additions & 15 deletions panel/src/ts/components/inputs/editor-input.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { $ } from "../../utils/selectors";
import { app } from "../../app";
import { CodeView } from "./editor/code/view";
import { type CodeView } from "./editor/code/view";
import { debounce } from "../../utils/events";
import { escapeRegExp } from "../../utils/validation";
import { insertIcon } from "../icons";
import { MarkdownView } from "./editor/markdown/view";
import { type MarkdownView } from "./editor/markdown/view";

function addBaseUri(markdown: string, baseUri: string) {
return markdown.replace(/(!\[.*\])\((?!https?:\/\/)([^)]+)\)/g, `$1(${baseUri}$2)`);
Expand Down Expand Up @@ -40,7 +40,9 @@ export class EditorInput {

private container: HTMLElement | null;

private editor: MarkdownView | CodeView;
private editor: MarkdownView | CodeView | undefined;

private editorPromise: Promise<void>;

constructor(textarea: HTMLTextAreaElement, options: Partial<EditorInputOptions> = {}) {
this.element = textarea;
Expand Down Expand Up @@ -99,32 +101,35 @@ export class EditorInput {
const codeSwitch = $("[data-command=toggle-markdown]", this.container) as HTMLButtonElement;

if (mode === "code") {
this.switchToCode();
this.editorPromise = this.switchToCode();
codeSwitch.classList.add("is-active");
} else {
this.switchToMarkdown();
this.editorPromise = this.switchToMarkdown();
codeSwitch.classList.remove("is-active");
}

codeSwitch.addEventListener("click", () => {
if (codeSwitch.classList.toggle("is-active")) {
this.switchToCode();
this.editorPromise = this.switchToCode();
window.localStorage.setItem(`formwork.editorMode[${key}]`, "code");
} else {
this.switchToMarkdown();
this.editorPromise = this.switchToMarkdown();
window.localStorage.setItem(`formwork.editorMode[${key}]`, "markdown");
}
this.editor.view.focus();
this.editorPromise.then(() => this.editor?.view.focus());
});

$(`label[for="${textarea.id}"]`)?.addEventListener("click", () => this.editor.view.focus());
$(`label[for="${textarea.id}"]`)?.addEventListener("click", () => {
this.editorPromise.then(() => this.editor?.view.focus());
});
}

switchToMarkdown() {
async switchToMarkdown() {
if (!this.container) {
return;
}
this.editor?.destroy();
const { MarkdownView } = await import("./editor/markdown/view");
this.editor = new MarkdownView(this.name, this.container, addBaseUri(this.element.value, this.options.baseUri), this.options.inputEventHandler, {
editable: !(this.element.disabled || this.element.readOnly),
placeholder: this.element.placeholder,
Expand All @@ -135,11 +140,12 @@ export class EditorInput {
this.editor.view.dom.style.height = `${this.options.height}px`;
}

switchToCode() {
async switchToCode() {
if (!this.container) {
return;
}
this.editor?.destroy();
const { CodeView } = await import("./editor/code/view");
this.editor = new CodeView(this.container, removeBaseUri(this.element.value, this.options.baseUri), this.options.inputEventHandler, {
editable: !(this.element.disabled || this.element.readOnly),
placeholder: this.element.placeholder,
Expand All @@ -156,22 +162,30 @@ export class EditorInput {
}

get value(): string {
return this.editor.content;
return this.editor?.content ?? this.element.value;
}

get disabled(): boolean {
return !this.editor.editable;
return this.editor ? !this.editor.editable : this.element.disabled;
}

set disabled(value: boolean) {
this.element.disabled = value;
this.editor.editable = !value;
this.editorPromise.then(() => {
if (this.editor) {
this.editor.editable = !value;
}
});
const toggleButton = $("[data-command=toggle-markdown]", this.container!) as HTMLButtonElement;
toggleButton.disabled = value;
}

set value(value: string) {
this.editor.content = value;
this.element.value = value;
this.editorPromise.then(() => {
if (this.editor) {
this.editor.content = value;
}
});
}
}
3 changes: 2 additions & 1 deletion panel/src/ts/components/inputs/editor/code/menu.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
import type { EditorView, ViewUpdate } from "@codemirror/view";
import { redo, redoDepth, undo, undoDepth } from "@codemirror/commands";
import { app } from "../../../../app";
import { passIcon } from "../../../icons";
import { ViewPlugin } from "@codemirror/view";

function createButton(icon: string, title: string) {
const btn = document.createElement("button");
Expand Down
6 changes: 3 additions & 3 deletions panel/src/ts/components/inputs/editor/markdown/commands.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command, EditorState } from "prosemirror-state";
import { Mark, MarkType, Node, NodeType } from "prosemirror-model";
import { EditorView } from "prosemirror-view";
import type { Command, EditorState } from "prosemirror-state";
import type { Mark, MarkType, Node, NodeType } from "prosemirror-model";
import type { EditorView } from "prosemirror-view";
export { lift, setBlockType, toggleMark, wrapIn } from "prosemirror-commands";
export { sinkListItem, wrapInList } from "prosemirror-schema-list";
import { redo as historyRedo, undo as historyUndo, redoDepth, undoDepth } from "prosemirror-history";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ellipsis, emDash, inputRules, smartQuotes, textblockTypeInputRule, wrappingInputRule } from "prosemirror-inputrules";
import { NodeType, Schema } from "prosemirror-model";
import type { NodeType, Schema } from "prosemirror-model";

export function blockQuoteRule(nodeType: NodeType) {
return wrappingInputRule(/^\s*>\s$/, nodeType);
Expand Down
4 changes: 2 additions & 2 deletions panel/src/ts/components/inputs/editor/markdown/keymap.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setBlockType, toggleMark, wrapIn } from "prosemirror-commands";
import { liftListItem, sinkListItem, splitListItem, wrapInList } from "prosemirror-schema-list";
import { redo, undo } from "prosemirror-history";
import { Command } from "prosemirror-state";
import { Schema } from "prosemirror-model";
import type { Command } from "prosemirror-state";
import type { Schema } from "prosemirror-model";
import { undoInputRule } from "prosemirror-inputrules";

const mac = typeof navigator !== "undefined" ? /Mac|iP(hone|[oa]d)/.test(navigator.platform) : false;
Expand Down
4 changes: 2 additions & 2 deletions panel/src/ts/components/inputs/editor/markdown/linktooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { debounce, throttle } from "../../../../utils/events";
import { getMarkRange, insertLink, removeLink } from "./commands";
import { $ } from "../../../../utils/selectors";
import { app } from "../../../../app";
import { EditorView } from "prosemirror-view";
import type { EditorView } from "prosemirror-view";
import { insertIcon } from "../../../icons";
import { Mark } from "prosemirror-model";
import type { Mark } from "prosemirror-model";
import { Plugin } from "prosemirror-state";
import { schema } from "prosemirror-markdown";
import { Tooltip } from "../../../tooltip";
Expand Down
7 changes: 4 additions & 3 deletions panel/src/ts/components/inputs/editor/markdown/menu.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Command, NodeSelection, Plugin } from "prosemirror-state";
import { insertImage, insertLink, isMarkActive, lift, redo, setBlockType, sinkListItem, toggleMark, undo, wrapIn, wrapInList } from "./commands";
import { MarkType, NodeType } from "prosemirror-model";
import type { MarkType, NodeType } from "prosemirror-model";
import { NodeSelection, Plugin } from "prosemirror-state";
import { $$ } from "../../../../utils/selectors";
import { app } from "../../../../app";
import { EditorView } from "prosemirror-view";
import type { Command } from "prosemirror-state";
import type { EditorView } from "prosemirror-view";
import { passIcon } from "../../../icons";
import { schema } from "prosemirror-markdown";

Expand Down
Loading
Loading