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
6 changes: 6 additions & 0 deletions demo/app/Sharp/Posts/PostForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use App\Sharp\Utils\Embeds\TableOfContentsEmbed;
use Code16\Sharp\Form\Eloquent\Uploads\Transformers\SharpUploadModelFormAttributeTransformer;
use Code16\Sharp\Form\Eloquent\WithSharpFormEloquentUpdater;
use Code16\Sharp\Form\Fields\Editor\TextInputReplacement\EditorTextInputReplacement;
use Code16\Sharp\Form\Fields\Editor\TextInputReplacement\EditorTextInputReplacementPreset;
use Code16\Sharp\Form\Fields\Editor\Uploads\SharpFormEditorUpload;
use Code16\Sharp\Form\Fields\SharpFormAutocompleteRemoteField;
use Code16\Sharp\Form\Fields\SharpFormCheckField;
Expand Down Expand Up @@ -72,6 +74,10 @@ public function buildFormFields(FieldsContainer $formFields): void
->setMaxFileSize(2)
->setHasLegend()
)
->setTextInputReplacements([
EditorTextInputReplacementPreset::frenchTypography(locale: 'fr', guillemets: true),
new EditorTextInputReplacement('/:\+1:/', '👍'),
])
->allowFullscreen()
->setMaxLength(2000)
->setHeight(300, 0)
Expand Down
13 changes: 12 additions & 1 deletion resources/js/form/components/fields/editor/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@
import EditorHelpText from "@/form/components/fields/editor/EditorHelpText.vue";
import FormFieldError from "@/form/components/FormFieldError.vue";
import EditorMaybeFullscreenDialog from "@/form/components/fields/editor/EditorMaybeFullscreenDialog.vue";
import { DecorateHiddenCharacters } from "@/form/components/fields/editor/extensions/DecorateHiddenCharacters";
import {
getTextInputReplacementsExtension
} from "@/form/components/fields/editor/extensions/TextInputReplacements";

const emit = defineEmits<FormFieldEmits<FormEditorFieldData>>();
const props = defineProps<FormFieldProps<FormEditorFieldData>>();
Expand Down Expand Up @@ -101,6 +105,13 @@
field.markdown && Markdown.configure({
breaks: config('sharp.markdown_editor.nl2br'),
}),
getTextInputReplacementsExtension(field, locale),
DecorateHiddenCharacters.configure({
class: cn(
`relative pl-[.125em] cursor-text after:block after:absolute after:top-1/2 after:-translate-y-1/2 after:left-1/2 after:-translate-x-1/2 after:opacity-25`,
`data-[key=nbsp]:after:content-['°']`,
),
}),
props.field.uploads && Upload.configure({
uploadManager,
locale,
Expand All @@ -121,7 +132,7 @@
? props.value?.text?.[locale] ?? ''
: props.value?.text ?? '',
editable: !field.readOnly,
enableInputRules: false,
enableInputRules: ['textInputReplacements'],
enablePasteRules: [Iframe],
extensions,
injectCSS: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Extension } from "@tiptap/core";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { Plugin } from "@tiptap/pm/state";
import { cn } from "@/utils/cn";

export const DecorateHiddenCharacters = Extension.create({
name: 'decorateHiddenCharacters',

addOptions() {
return {
class: '',
}
},


addProseMirrorPlugins() {
const buildDecorations = (doc) => {
const decorations = []


doc.descendants((node, pos) => {
if (!node.isText) return true


const text = node.text
if (!text) return true

let idx = text.indexOf('\u00A0')
while (idx !== -1) {
const from = pos + idx
const to = from + 1
const deco = Decoration.inline(from, to, {
'data-key': 'nbsp',
class: this.options.class,
})
decorations.push(deco)
idx = text.indexOf('\u00A0', idx + 1)
}


return true
})


return DecorationSet.create(doc, decorations)
}


return [
new Plugin({
state: {
init(_, { doc }) {
return buildDecorations(doc)
},
apply(tr, old) {
if (!tr.docChanged) return old
return buildDecorations(tr.doc)
},
},
props: {
decorations(state) {
return this.getState(state)
},
},
}),
]
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Extension, textInputRule } from "@tiptap/core";
import { FormEditorFieldData } from "@/types";


export function getTextInputReplacementsExtension(field: FormEditorFieldData, locale: string) {
return Extension.create({
name: 'textInputReplacements',
addInputRules() {
return field.textInputReplacements
.filter(replacement => !replacement.locale || replacement.locale === locale)
.map(replacement => {
const pattern = replacement.pattern.replace(/^\//, '').replace(/\/$/, '').replace(/\$?$/, '$');
try {
return textInputRule({
find: new RegExp(pattern, 'u'),
replace: replacement.replacement,
});
} catch (e) {
console.error(e);
}
return null;
})
.filter(Boolean);
},
});
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Extension, getExtensionField, getSchema } from "@tiptap/core";
import { Extension, getExtensionField, getSchema, textInputRule } from "@tiptap/core";
import { Document } from '@tiptap/extension-document';
import { Text } from '@tiptap/extension-text';
import { Paragraph } from '@tiptap/extension-paragraph';
Expand Down
8 changes: 5 additions & 3 deletions resources/js/types/generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ export type FormAutocompleteLocalFieldData = {
resultItemTemplate: string | null;
templateData: { [key: string]: any } | null;
searchKeys: Array<string> | null;
localized: boolean | null;
dynamicAttributes: Array<FormDynamicAttributeData> | null;
label: string | null;
readOnly: boolean | null;
Expand Down Expand Up @@ -369,6 +368,11 @@ export type FormEditorFieldData = {
inline: boolean;
showCharacterCount: boolean;
allowFullscreen: boolean;
textInputReplacements: {
pattern: string;
replacement: string;
locale?: string;
}[];
uploads: FormEditorFieldUploadData | null;
embeds: { [embedKey: string]: EmbedData };
toolbar: Array<FormEditorToolbarButton | `embed:${string}`>;
Expand Down Expand Up @@ -544,7 +548,6 @@ export type FormSelectFieldData = {
inline: boolean;
dynamicAttributes: Array<FormDynamicAttributeData> | null;
maxSelected: number | null;
localized: boolean | null;
label: string | null;
readOnly: boolean | null;
conditionalDisplay: FormConditionalDisplayData | null;
Expand All @@ -560,7 +563,6 @@ export type FormTagsFieldData = {
options: Array<{ id: string | number; label: string }>;
maxTagCount: number | null;
placeholder: string | null;
localized: boolean | null;
label: string | null;
readOnly: boolean | null;
conditionalDisplay: FormConditionalDisplayData | null;
Expand Down
2 changes: 2 additions & 0 deletions src/Data/Form/Fields/FormEditorFieldData.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public function __construct(
public bool $inline,
public bool $showCharacterCount,
public bool $allowFullscreen,
#[LiteralTypeScriptType('{ pattern: string, replacement: string, locale?: string }[]')]
public array $textInputReplacements,
#[LiteralTypeScriptType('FormEditorFieldUploadData | null')]
public ?array $uploads = null,
#[LiteralTypeScriptType('{ [embedKey:string]:EmbedData }')]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Code16\Sharp\Form\Fields\Editor\TextInputReplacement\Concerns;

use Code16\Sharp\Form\Fields\Editor\TextInputReplacement\EditorTextInputReplacement;

/**
* @internal
*/
trait ReplacesFrench
{
public static function frenchTypography(
?string $locale = null,
bool $nbsp = true,
bool $guillemets = false,
): self {
return (new self())
->when($nbsp)->add(new EditorTextInputReplacement('/( )[!?:;»]/', ' ', $locale))
->when($guillemets)->add(new EditorTextInputReplacement('/(["«][^\n\S])/', '« ', $locale))
->when($guillemets)->add(new EditorTextInputReplacement('/[«][^\n\S][^»]+([^\n\S]")/', ' »', $locale))
->when($guillemets)->add(new EditorTextInputReplacement('/[«][^\n\S][^»]+(")/', ' »', $locale));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Code16\Sharp\Form\Fields\Editor\TextInputReplacement;

use Illuminate\Contracts\Support\Arrayable;

class EditorTextInputReplacement implements Arrayable
{
/**
* @throws \Exception
*/
public function __construct(
protected string $pattern,
protected string $replacement,
protected ?string $locale = null,
) {
if(!str_starts_with($pattern, '/') || !str_ends_with($pattern, '/')) {
throw new \Exception("The replacement pattern \"$pattern\" must start and end with a slash");
}
}

public function toArray(): array
{
return [
'pattern' => $this->pattern,
'replacement' => $this->replacement,
'locale' => $this->locale,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Code16\Sharp\Form\Fields\Editor\TextInputReplacement;

use Code16\Sharp\Form\Fields\Editor\TextInputReplacement\Concerns\ReplacesFrench;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Traits\Conditionable;
use Illuminate\Support\Traits\Macroable;

class EditorTextInputReplacementPreset implements Arrayable
{
use Conditionable;
use Macroable;
use ReplacesFrench;

public function __construct(
protected array $replacements = [],
) {}

public function add(EditorTextInputReplacement $replacement): self
{
$this->replacements[] = $replacement;

return $this;
}

public function toArray(): array
{
return collect($this->replacements)
->map(fn (EditorTextInputReplacement $replacement) => $replacement->toArray())
->all();
}
}
16 changes: 16 additions & 0 deletions src/Form/Fields/SharpFormEditorField.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

use Code16\Sharp\Enums\FormEditorToolbarButton;
use Code16\Sharp\Exceptions\SharpInvalidConfigException;
use Code16\Sharp\Form\Fields\Editor\TextInputReplacement\EditorTextInputReplacement;
use Code16\Sharp\Form\Fields\Editor\TextInputReplacement\EditorTextInputReplacementPreset;
use Code16\Sharp\Form\Fields\Editor\Uploads\FormEditorUploadForm;
use Code16\Sharp\Form\Fields\Editor\Uploads\SharpFormEditorUpload;
use Code16\Sharp\Form\Fields\Formatters\EditorFormatter;
Expand Down Expand Up @@ -66,6 +68,7 @@ class SharpFormEditorField extends SharpFormField implements IsSharpFieldWithEmb
protected bool $withoutParagraphs = false;
protected bool $showCharacterCount = false;
protected bool $allowFullscreen = false;
protected array $textInputReplacements = [];

protected function __construct(string $key, string $type, ?SharpFieldFormatter $formatter = null)
{
Expand Down Expand Up @@ -141,6 +144,13 @@ public function setRenderContentAsMarkdown(bool $renderAsMarkdown = true): self
return $this;
}

public function setTextInputReplacements(array $replacements): self
{
$this->textInputReplacements = $replacements;

return $this;
}

public function allowFullscreen(bool $allowFullscreen = true): self
{
$this->allowFullscreen = $allowFullscreen;
Expand Down Expand Up @@ -260,6 +270,12 @@ public function toArray(): array
'showCharacterCount' => $this->showCharacterCount,
'maxLength' => $this->maxLength,
'allowFullscreen' => $this->allowFullscreen,
'textInputReplacements' => collect($this->textInputReplacements)
->flatMap(fn (EditorTextInputReplacement|EditorTextInputReplacementPreset $replacement) => $replacement instanceof EditorTextInputReplacementPreset
? $replacement->toArray()
: [$replacement->toArray()]
)
->all(),
'uploads' => $this->innerComponentUploadsConfiguration(),
'embeds' => $this->innerComponentEmbedsConfiguration(),
],
Expand Down
18 changes: 18 additions & 0 deletions tests/Unit/Form/Fields/SharpFormEditorFieldTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php

use Code16\Sharp\Exceptions\SharpInvalidConfigException;
use Code16\Sharp\Form\Fields\Editor\TextInputReplacement\EditorTextInputReplacement;
use Code16\Sharp\Form\Fields\Editor\TextInputReplacement\EditorTextInputReplacementPreset;
use Code16\Sharp\Form\Fields\Editor\Uploads\SharpFormEditorUpload;
use Code16\Sharp\Form\Fields\SharpFormEditorField;
use Code16\Sharp\Tests\Unit\Form\Fields\Formatters\Fixtures\EditorFormatterTestEmbed;
Expand All @@ -25,6 +27,7 @@
'markdown' => false,
'inline' => false,
'allowFullscreen' => false,
'textInputReplacements' => [],
]);
});

Expand Down Expand Up @@ -177,3 +180,18 @@

$formField->toArray();
})->throws(SharpInvalidConfigException::class);

it('allows to define text input replacements', function () {
$formField = SharpFormEditorField::make('text')
->setTextInputReplacements([
new EditorTextInputReplacementPreset([
new EditorTextInputReplacement('/(c)/', '©'),
]),
new EditorTextInputReplacement('/(r)/', '®'),
]);

expect($formField->toArray()['textInputReplacements'])->toEqual([
['pattern' => '/(c)/', 'replacement' => '©', 'locale' => null],
['pattern' => '/(r)/', 'replacement' => '®', 'locale' => null],
]);
});
Loading