mirror of
https://github.com/directus/directus.git
synced 2026-04-03 03:00:39 -04:00
Move in block editor exclusive extension (#18525)
* move in block editor * make lodash import consistent with core * add translations * add preview svg * add changeset * change interface id * Update .changeset/ten-wasps-relate.md * removed unneeded property definitions. * address couple of warnings * tweak checkbox color * Check if file has been selected * Update style overrides * Override slightly blue background * Add red background for delete confirmation * Fix table add row and column button background color * override checklist color when hovered * override color of ripple effect for checkbox * Fix config being undefined * tweak popover style * fix attaches tool * Revert fix * fix attaches styling * Fix inline selection active color * fix attaches download button * tweak attaches file styling * Fix alignment icons font colour * remove nullable prop Co-authored-by: ian <licitdev@gmail.com> * tiny code style tweak --------- Co-authored-by: Brainslug <tim@brainslug.nl> Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch> Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com> Co-authored-by: ian <licitdev@gmail.com>
This commit is contained in:
5
.changeset/ten-wasps-relate.md
Normal file
5
.changeset/ten-wasps-relate.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@directus/app': minor
|
||||
---
|
||||
|
||||
Added block editor interface
|
||||
@@ -37,6 +37,21 @@
|
||||
"@directus/format-title": "10.0.0",
|
||||
"@directus/types": "workspace:*",
|
||||
"@directus/utils": "workspace:*",
|
||||
"@editorjs/attaches": "1.3.0",
|
||||
"@editorjs/checklist": "1.5.0",
|
||||
"@editorjs/code": "2.8.0",
|
||||
"@editorjs/delimiter": "1.3.0",
|
||||
"@editorjs/editorjs": "2.26.5",
|
||||
"@editorjs/embed": "2.5.3",
|
||||
"@editorjs/header": "2.7.0",
|
||||
"@editorjs/image": "2.8.1",
|
||||
"@editorjs/inline-code": "1.4.0",
|
||||
"@editorjs/nested-list": "1.3.0",
|
||||
"@editorjs/paragraph": "2.9.0",
|
||||
"@editorjs/quote": "2.5.0",
|
||||
"@editorjs/raw": "2.4.0",
|
||||
"@editorjs/table": "2.2.1",
|
||||
"@editorjs/underline": "1.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.4.0",
|
||||
"@fullcalendar/core": "6.1.5",
|
||||
@@ -100,6 +115,8 @@
|
||||
"diacritics": "1.3.0",
|
||||
"diff": "5.1.0",
|
||||
"dompurify": "3.0.2",
|
||||
"editorjs-text-alignment-blocktune": "1.0.3",
|
||||
"editorjs-toggle-block": "0.3.11",
|
||||
"escape-string-regexp": "5.0.0",
|
||||
"file-saver": "2.0.5",
|
||||
"flatpickr": "4.6.13",
|
||||
|
||||
615
app/src/interfaces/input-block-editor/editorjs-overrides.css
Normal file
615
app/src/interfaces/input-block-editor/editorjs-overrides.css
Normal file
@@ -0,0 +1,615 @@
|
||||
.codex-editor {
|
||||
--bg-color: var(--background-normal) !important;
|
||||
--front-color: var(--foreground-normal) !important;
|
||||
--border-color: var(--border-normal) !important;
|
||||
}
|
||||
|
||||
.codex-editor .ce-toolbar__plus,
|
||||
.codex-editor .ce-toolbar__settings-btn {
|
||||
color: var(--foreground-normal);
|
||||
background-color: var(--background-normal);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.codex-editor .ce-toolbar__plus:hover,
|
||||
.codex-editor .ce-toolbar__settings-btn:hover {
|
||||
background-color: var(--background-normal-alt);
|
||||
}
|
||||
|
||||
.codex-editor .ce-toolbar__plus svg,
|
||||
.codex-editor .ce-toolbar__settings-btn svg {
|
||||
fill: var(--foreground-normal) !important;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-settings-button svg,
|
||||
.codex-editor .ce-inline-toolbar__dropdown svg,
|
||||
.codex-editor .ce-inline-toolbar .ce-inline-tool svg,
|
||||
.codex-editor .ce-settings .ce-settings__button svg {
|
||||
fill: var(--v-list-color) !important;
|
||||
}
|
||||
|
||||
.codex-editor .ce-inline-toolbar .ce-inline-tool--active svg,
|
||||
.codex-editor .ce-settings .cdx-settings-button--active svg {
|
||||
fill: var(--primary) !important;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-settings-button--active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.codex-editor .ce-toolbar .ce-toolbox__button svg,
|
||||
.codex-editor .ce-toolbar .ce-toolbox__button:hover svg {
|
||||
fill: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-loader::before {
|
||||
border: 3px solid var(--background-normal-alt);
|
||||
border-left-color: var(--primary);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-search-field {
|
||||
border-radius: var(--border-radius) !important;
|
||||
border: none;
|
||||
background: var(--background-subdued);
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover {
|
||||
background: var(--background-page);
|
||||
border-color: var(--border-normal);
|
||||
border-width: var(--border-width);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover__item--focused {
|
||||
--webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
background: var(--v-list-background-color-active) !important;
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover__item--confirmation .ce-popover__item-label {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover__item--active {
|
||||
color: var(--primary);
|
||||
background: var(--v-list-background-color-active);
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover__item:hover {
|
||||
background: var(--v-list-background-color-hover) !important;
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover__item--confirmation,
|
||||
.codex-editor .ce-popover__item--confirmation:hover {
|
||||
background: var(--danger-alt) !important;
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover__item-icon {
|
||||
background: var(--background-page);
|
||||
border: var(--border-width) solid var(--border-normal);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover__item-icon svg {
|
||||
fill: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover__item-secondary-label {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
.codex-editor .ce-popover__items:not(:first-child) {
|
||||
border-top: var(--border-width) solid var(--border-normal);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.codex-editor .image-tool--loading .image-tool__image,
|
||||
.codex-editor .ce-block--selected .ce-block__content {
|
||||
background: var(--module-background-alt);
|
||||
}
|
||||
|
||||
.codex-editor .image-tool--loading .image-tool__image {
|
||||
border-color: var(--border-normal);
|
||||
}
|
||||
|
||||
.codex-editor .ce-inline-toolbar__toggler-and-button-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.codex-editor .ce-inline-toolbar__dropdown {
|
||||
margin: 0;
|
||||
border-color: var(--border-normal);
|
||||
}
|
||||
|
||||
.codex-editor .ce-inline-tool {
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
.codex-editor .ce-settings__button--confirm.ce-settings__button--confirm {
|
||||
background-color: var(--danger-110) !important;
|
||||
}
|
||||
|
||||
.codex-editor .ce-settings__button--confirm.ce-settings__button--confirm svg {
|
||||
fill: var(--foreground-normal-alt) !important;
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
.ct {
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ct:before,
|
||||
.ct:after {
|
||||
background-color: var(--background-inverted);
|
||||
}
|
||||
|
||||
.ct__content {
|
||||
background-color: var(--background-inverted);
|
||||
color: var(--foreground-inverted);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* Button balloons */
|
||||
|
||||
.codex-editor .ce-toolbox__button.ce-toolbox__button--active {
|
||||
background: var(--background-normal-alt);
|
||||
}
|
||||
|
||||
.codex-editor .ce-toolbox__button.ce-toolbox__button--active svg {
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
.codex-editor .ce-conversion-toolbar,
|
||||
.codex-editor .ce-inline-toolbar,
|
||||
.codex-editor .ce-settings {
|
||||
background: var(--background-subdued);
|
||||
border-color: var(--border-normal);
|
||||
border-width: var(--border-width);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-settings-button:hover,
|
||||
.codex-editor .ce-conversion-tool:hover,
|
||||
.codex-editor .ce-inline-toolbar__dropdown:hover,
|
||||
.codex-editor .ce-inline-toolbar .ce-inline-tool:hover,
|
||||
.codex-editor .ce-settings .ce-settings__button:hover {
|
||||
background: var(--background-normal-alt);
|
||||
}
|
||||
|
||||
.codex-editor .ce-conversion-toolbar .ce-conversion-tool--focused:not(:hover) {
|
||||
--webkit-box-shadow: unset;
|
||||
|
||||
color: var(--primary);
|
||||
background-color: var(--v-list-item-background-color-hover) !important;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.codex-editor .ce-conversion-tool:hover {
|
||||
--webkit-box-shadow: unset;
|
||||
|
||||
color: var(--v-list-item-color-hover);
|
||||
background-color: var(--v-list-item-background-color-hover) !important;
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.codex-editor .ce-conversion-toolbar__label {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.codex-editor .ce-conversion-tool__icon {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Textarea and inputs */
|
||||
|
||||
.codex-editor [contentEditable='true'][data-placeholder]::before {
|
||||
font-weight: unset;
|
||||
}
|
||||
|
||||
.codex-editor .ce-code__textarea {
|
||||
font-family: var(--family-monospace);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-input,
|
||||
.codex-editor .ce-code__textarea {
|
||||
color: var(--foreground-normal);
|
||||
font-size: inherit;
|
||||
background-color: var(--background-page);
|
||||
border: var(--border-width) solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-input:hover,
|
||||
.codex-editor .ce-code__textarea:hover {
|
||||
border-color: var(--border-normal-alt);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-input:focus,
|
||||
.codex-editor .ce-code__textarea:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.codex-editor .ce-inline-tool-input {
|
||||
color: var(--primary);
|
||||
background: var(--background-normal);
|
||||
border-top-color: var(--border-normal);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
.codex-editor .cdx-button {
|
||||
color: var(--v-button-color);
|
||||
font-weight: var(--v-button-font-weight);
|
||||
font-size: var(--v-button-font-size);
|
||||
text-decoration: none;
|
||||
background-color: var(--v-button-background-color);
|
||||
border: var(--border-width) solid var(--v-button-background-color);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-button:hover {
|
||||
color: var(--v-button-color-hover);
|
||||
background-color: var(--v-button-background-color-hover);
|
||||
border-color: var(--v-button-background-color-hover);
|
||||
}
|
||||
|
||||
/* General components re-style */
|
||||
|
||||
.codex-editor .cdx-attaches,
|
||||
.codex-editor .cdx-personality {
|
||||
color: var(--foreground-normal);
|
||||
background: var(--background-normal);
|
||||
border-color: var(--border-normal);
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
/* Paragraph */
|
||||
|
||||
.codex-editor .ce-paragraph {
|
||||
padding: 0;
|
||||
line-height: inherit;
|
||||
}
|
||||
.codex-editor .ce-paragraph[data-placeholder]:empty::before {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
/* Quote block */
|
||||
|
||||
.codex-editor .cdx-block.cdx-quote {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
/* Image Tool */
|
||||
|
||||
.codex-editor .image-tool {
|
||||
--bg-color: var(--background-normal-alt);
|
||||
--front-color: var(--primary);
|
||||
--border-color: var(--border-normal);
|
||||
}
|
||||
|
||||
/* Personality */
|
||||
|
||||
.codex-editor .cdx-personality__description {
|
||||
font-weight: unset;
|
||||
font-size: 0.9em;
|
||||
font-style: oblique;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-personality .cdx-personality__photo {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-personality__link {
|
||||
color: var(--primary);
|
||||
font-size: 0.72em;
|
||||
}
|
||||
|
||||
/* Checklist */
|
||||
|
||||
.codex-editor .cdx-checklist__item-checkbox {
|
||||
background: transparent;
|
||||
border-color: var(--border-normal);
|
||||
border-width: var(--border-width);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-checklist__item-text {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
/* Ripple effect when enabling a checkbox */
|
||||
.codex-editor .cdx-checklist__item-checkbox .cdx-checklist__item-checkbox-check::before {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-checklist__item--checked .cdx-checklist__item-checkbox .cdx-checklist__item-checkbox-check {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Checkbox color when hovered. We have to use !important here because the specificity of the default styling is higher */
|
||||
.codex-editor .cdx-checklist__item--checked .cdx-checklist__item-checkbox:hover .cdx-checklist__item-checkbox-check {
|
||||
background: var(--primary-125) !important;
|
||||
border-color: var(--primary-125) !important;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-checklist__item--checked .cdx-checklist__item-checkbox::after {
|
||||
top: 4px;
|
||||
left: 3px;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
|
||||
.codex-editor .ce-block ul,
|
||||
.codex-editor .ce-block ol {
|
||||
font-size: inherit !important;
|
||||
}
|
||||
|
||||
/* Attaches */
|
||||
|
||||
.codex-editor .cdx-attaches--with-file {
|
||||
border: var(--border-width) solid var(--border-normal);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-attaches--with-file .cdx-attaches__file-icon {
|
||||
padding: 5px;
|
||||
background: var(--background-normal-alt);
|
||||
border-radius: var(--border-radius);
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-attaches--with-file .cdx-attaches__file-icon::before {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-attaches--with-file .cdx-attaches__file-icon-background {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.codex-editor .cdx-attaches--with-file .cdx-attaches__title {
|
||||
color: var(--foreground-normal);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-attaches--with-file .cdx-attaches__download-button {
|
||||
background: var(--background-subdued);
|
||||
border: var(--border-width) solid var(--border-normal);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-attaches--with-file .cdx-attaches__download-button:hover {
|
||||
background: var(--background-highlight);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-attaches--with-file .cdx-attaches__download-button svg {
|
||||
fill: var(--v-button-background-color);
|
||||
}
|
||||
|
||||
.codex-editor .cdx-attaches__size {
|
||||
color: var(--foreground-subdued);
|
||||
}
|
||||
|
||||
/* InlineCode */
|
||||
|
||||
.codex-editor .ce-block .inline-code {
|
||||
padding: 4px;
|
||||
color: var(--v-chip-background-color);
|
||||
font-family: monospace;
|
||||
background-color: var(--v-chip-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* tc module */
|
||||
|
||||
.codex-editor .tc-wrap {
|
||||
--color-background: var(--v-list-item-background-color-active);
|
||||
--color-border: var(--border-normal);
|
||||
--color-background-hover: var(--v-list-item-background-color-hover);
|
||||
}
|
||||
|
||||
.codex-editor .tc-popover {
|
||||
--color-background: var(--background-normal);
|
||||
--color-border: var(--background-normal);
|
||||
--color-background-hover: var(--background-normal-alt);
|
||||
}
|
||||
|
||||
.tc-cell--selected,
|
||||
.tc-cell--selected::after,
|
||||
.tc-row--selected,
|
||||
.tc-row--selected::after {
|
||||
background: var(--background-highlight);
|
||||
}
|
||||
|
||||
.tc-toolbox {
|
||||
--toggler-dots-color: var(--foreground-normal);
|
||||
--toggler-dots-color-hovered: var(--foreground-normal-alt);
|
||||
}
|
||||
|
||||
.tc-toolbox__toggler:hover {
|
||||
fill: var(--background-normal-alt) !important;
|
||||
}
|
||||
|
||||
.codex-editor .tc-toolbox__toggler svg rect:first-child {
|
||||
/* This is very ugly, but there no other way to set background of the element */
|
||||
fill: var(--background-normal) !important;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.codex-editor .tc-toolbox__toggler:hover svg rect:first-child {
|
||||
/* This is very ugly, but there no other way to set background of the element */
|
||||
fill: var(--background-normal-alt) !important;
|
||||
}
|
||||
|
||||
.tc-add-column:hover,
|
||||
.tc-add-row:hover {
|
||||
background-color: var(--primary-alt);
|
||||
}
|
||||
|
||||
.tc-add-row:hover::before {
|
||||
background-color: var(--v-list-item-background-color-hover);
|
||||
}
|
||||
|
||||
.interface.subdued .codex-editor {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.ce-toolbar__content,
|
||||
.ce-block__content {
|
||||
max-width: unset;
|
||||
margin-right: 34px;
|
||||
margin-left: 34px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-header {
|
||||
padding: 0.4em 0;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
font-weight: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block h1 {
|
||||
font-weight: 300;
|
||||
font-size: 44px;
|
||||
line-height: 52px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block h2 {
|
||||
font-weight: 600;
|
||||
font-size: 34px;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block h3 {
|
||||
font-weight: 600;
|
||||
font-size: 26px;
|
||||
line-height: 31px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block h4 {
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block h5 {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block h6 {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block p {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
font-family: inherit;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block a {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.codex-editor .ce-block a.btn {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block ul,
|
||||
.codex-editor .ce-block ol {
|
||||
margin: 24px 0;
|
||||
font-size: 18px;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block ul ul,
|
||||
.codex-editor .ce-block ol ol,
|
||||
.codex-editor .ce-block ul ol,
|
||||
.codex-editor .ce-block ol ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block b,
|
||||
.codex-editor .ce-block strong {
|
||||
color: var(--foreground-normal-alt);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block pre {
|
||||
padding: 20px;
|
||||
overflow: auto;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
background-color: var(--background-normal-alt);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block blockquote {
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
line-height: 34px;
|
||||
border-left: 2px dashed var(--border-normal);
|
||||
}
|
||||
|
||||
.codex-editor .ce-block video,
|
||||
.codex-editor .ce-block iframe,
|
||||
.codex-editor .ce-block img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block hr {
|
||||
margin-top: 52px;
|
||||
margin-bottom: 56px;
|
||||
text-align: center;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block hr::after {
|
||||
font-size: 28px;
|
||||
line-height: 0;
|
||||
letter-spacing: 16px;
|
||||
content: '...';
|
||||
}
|
||||
|
||||
.codex-editor .ce-block table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block table th,
|
||||
.codex-editor .ce-block table td {
|
||||
border: 1px solid var(--border-normal);
|
||||
}
|
||||
|
||||
.codex-editor .ce-block .tc-table__wrap,
|
||||
.codex-editor .ce-block .tc-table__cell {
|
||||
border-color: var(--border-normal);
|
||||
}
|
||||
|
||||
.codex-editor .ce-block figure {
|
||||
display: table;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.codex-editor .ce-block figure figcaption {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
color: var(--foreground-subdued);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.codex-editor .ce-inline-tool--active {
|
||||
color: var(--primary);
|
||||
}
|
||||
163
app/src/interfaces/input-block-editor/index.ts
Normal file
163
app/src/interfaces/input-block-editor/index.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { defineInterface } from '@directus/utils';
|
||||
import InterfaceBlockEditor from './input-block-editor.vue';
|
||||
import PreviewSVG from './preview.svg?raw';
|
||||
|
||||
export default defineInterface({
|
||||
id: 'input-block-editor',
|
||||
name: '$t:interfaces.input-block-editor.input-block-editor',
|
||||
description: '$t:interfaces.input-block-editor.description',
|
||||
icon: 'code',
|
||||
component: InterfaceBlockEditor,
|
||||
types: ['json'],
|
||||
group: 'standard',
|
||||
preview: PreviewSVG,
|
||||
options: [
|
||||
{
|
||||
field: 'placeholder',
|
||||
name: '$t:placeholder',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'text-input',
|
||||
options: {
|
||||
placeholder: '$t:enter_a_placeholder',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'font',
|
||||
name: '$t:font',
|
||||
type: 'string',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'select-dropdown',
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
text: '$t:sans_serif',
|
||||
value: 'sans-serif',
|
||||
},
|
||||
{
|
||||
text: '$t:monospace',
|
||||
value: 'monospace',
|
||||
},
|
||||
{
|
||||
text: '$t:serif',
|
||||
value: 'serif',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
default_value: 'sans-serif',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'tools',
|
||||
name: '$t:interfaces.input-block-editor.tools',
|
||||
type: 'json',
|
||||
schema: {
|
||||
default_value: ['header', 'nestedlist', 'code', 'image', 'paragraph', 'checklist', 'quote', 'underline'],
|
||||
},
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'select-multiple-dropdown',
|
||||
options: {
|
||||
choices: [
|
||||
{
|
||||
value: 'header',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.header',
|
||||
},
|
||||
{
|
||||
value: 'nestedlist',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.nestedlist',
|
||||
},
|
||||
{
|
||||
value: 'embed',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.embed',
|
||||
},
|
||||
{
|
||||
value: 'paragraph',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.paragraph',
|
||||
},
|
||||
{
|
||||
value: 'code',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.code',
|
||||
},
|
||||
{
|
||||
value: 'image',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.image',
|
||||
},
|
||||
{
|
||||
value: 'attaches',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.attaches',
|
||||
},
|
||||
{
|
||||
value: 'table',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.table',
|
||||
},
|
||||
{
|
||||
value: 'quote',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.quote',
|
||||
},
|
||||
{
|
||||
value: 'underline',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.underline',
|
||||
},
|
||||
{
|
||||
value: 'inlinecode',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.inlinecode',
|
||||
},
|
||||
{
|
||||
value: 'delimiter',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.delimiter',
|
||||
},
|
||||
{
|
||||
value: 'checklist',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.checklist',
|
||||
},
|
||||
{
|
||||
value: 'toggle',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.toggle',
|
||||
},
|
||||
{
|
||||
value: 'alignment',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.alignment',
|
||||
},
|
||||
{
|
||||
value: 'raw',
|
||||
text: '$t:interfaces.input-block-editor.tools_options.raw',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'bordered',
|
||||
name: '$t:displays.formatted-value.border',
|
||||
type: 'boolean',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'boolean',
|
||||
options: {
|
||||
label: '$t:displays.formatted-value.border_label',
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
default_value: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'folder',
|
||||
name: '$t:interfaces.system-folder.folder',
|
||||
type: 'uuid',
|
||||
meta: {
|
||||
width: 'full',
|
||||
interface: 'system-folder',
|
||||
note: '$t:interfaces.system-folder.field_hint',
|
||||
},
|
||||
schema: {
|
||||
default_value: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
250
app/src/interfaces/input-block-editor/input-block-editor.vue
Normal file
250
app/src/interfaces/input-block-editor/input-block-editor.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div class="input-block-editor">
|
||||
<div ref="editorElement" :class="{ [font]: true, disabled, bordered }"></div>
|
||||
|
||||
<v-drawer
|
||||
v-if="haveFilesAccess && !disabled"
|
||||
:model-value="fileHandler !== null"
|
||||
icon="image"
|
||||
:title="t('upload_from_device')"
|
||||
:cancelable="true"
|
||||
@update:model-value="unsetFileHandler"
|
||||
@cancel="unsetFileHandler"
|
||||
>
|
||||
<div class="uploader-drawer-content">
|
||||
<div v-if="currentPreview" class="uploader-preview-image">
|
||||
<img :src="currentPreview" />
|
||||
</div>
|
||||
<v-upload
|
||||
:ref="uploaderComponentElement"
|
||||
:multiple="false"
|
||||
:folder="folder"
|
||||
from-library
|
||||
from-url
|
||||
@input="handleFile"
|
||||
/>
|
||||
</div>
|
||||
</v-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api, { addTokenToURL } from '@/api';
|
||||
import EditorJS from '@editorjs/editorjs';
|
||||
import { isEqual, cloneDeep } from 'lodash';
|
||||
import { useFileHandler } from './use-file-handler';
|
||||
import getTools from './tools';
|
||||
import { useCollectionsStore } from '@/stores/collections';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
autofocus?: boolean;
|
||||
value?: Record<string, any> | null;
|
||||
bordered?: boolean;
|
||||
placeholder?: string;
|
||||
tools?: string[];
|
||||
folder?: string;
|
||||
font?: 'sans-serif' | 'monospace' | 'serif';
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
autofocus: false,
|
||||
value: () => null,
|
||||
bordered: true,
|
||||
tools: () => ['header', 'nestedlist', 'code', 'image', 'paragraph', 'checklist', 'quote', 'underline'],
|
||||
font: 'sans-serif',
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits(['input']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const collectionStore = useCollectionsStore();
|
||||
|
||||
const { currentPreview, setCurrentPreview, fileHandler, setFileHandler, unsetFileHandler, handleFile } =
|
||||
useFileHandler();
|
||||
|
||||
const editorjsRef = ref<EditorJS>();
|
||||
const uploaderComponentElement = ref<HTMLElement>();
|
||||
const editorElement = ref<HTMLElement>();
|
||||
const haveFilesAccess = Boolean(collectionStore.getCollection('directus_files'));
|
||||
const haveValuesChanged = ref<boolean>(false);
|
||||
|
||||
const tools = getTools(
|
||||
{
|
||||
addTokenToURL,
|
||||
baseURL: api.defaults.baseURL,
|
||||
setFileHandler,
|
||||
setCurrentPreview,
|
||||
getUploadFieldElement: () => uploaderComponentElement,
|
||||
},
|
||||
props.tools,
|
||||
haveFilesAccess
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
const sanitizedValue = sanitizeValue(props.value);
|
||||
|
||||
editorjsRef.value = new EditorJS({
|
||||
logLevel: 'ERROR' as EditorJS.LogLevels,
|
||||
holder: editorElement.value,
|
||||
readOnly: false,
|
||||
placeholder: props.placeholder,
|
||||
minHeight: 72,
|
||||
onChange: (api: any, event: any) => emitValue(api, event),
|
||||
tools: tools,
|
||||
});
|
||||
|
||||
// we have initial data, so we render it once the editor is ready...
|
||||
await editorjsRef.value.isReady;
|
||||
|
||||
if (sanitizedValue) {
|
||||
await editorjsRef.value.render(sanitizedValue);
|
||||
}
|
||||
|
||||
if (props.autofocus) {
|
||||
editorjsRef.value.focus();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (!editorjsRef.value) return;
|
||||
|
||||
editorjsRef.value.destroy();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
async (newVal: any, oldVal: any) => {
|
||||
if (!editorjsRef.value || !editorjsRef.value.isReady || haveValuesChanged.value) return;
|
||||
|
||||
if (fileHandler.value !== null) return;
|
||||
|
||||
if (isEqual(newVal?.blocks, oldVal?.blocks)) return;
|
||||
|
||||
try {
|
||||
await editorjsRef.value.isReady;
|
||||
const sanitizedValue = sanitizeValue(newVal);
|
||||
|
||||
if (sanitizedValue) {
|
||||
await editorjsRef.value.render(sanitizedValue);
|
||||
} else {
|
||||
editorjsRef.value.clear();
|
||||
}
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
}
|
||||
|
||||
haveValuesChanged.value = false;
|
||||
}
|
||||
);
|
||||
|
||||
async function emitValue(context: EditorJS.API, _event: CustomEvent) {
|
||||
if (props.disabled || !context || !context.saver) return;
|
||||
haveValuesChanged.value = true;
|
||||
|
||||
try {
|
||||
const result: EditorJS.OutputData = await context.saver.save();
|
||||
|
||||
if (!result || result.blocks.length < 1) {
|
||||
emit('input', null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEqual(result.blocks, props.value?.blocks)) return;
|
||||
|
||||
emit('input', result);
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeValue(value: any): EditorJS.OutputData | null {
|
||||
if (!value || typeof value !== 'object' || !value.blocks || value.blocks.length < 1) return null;
|
||||
|
||||
// we use cloneDeep to recursively clone the object
|
||||
return cloneDeep({
|
||||
time: value?.time || Date.now(),
|
||||
version: value?.version || '0.0.0',
|
||||
blocks: value.blocks,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './editorjs-overrides.css';
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.btn--default {
|
||||
color: #fff !important;
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
.btn--gray {
|
||||
color: #fff !important;
|
||||
background-color: #7c7c7c;
|
||||
border-color: #7c7c7c;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: var(--foreground-subdued);
|
||||
background-color: var(--background-subdued);
|
||||
border-color: var(--border-normal);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bordered {
|
||||
padding: var(--input-padding) 4px var(--input-padding) calc(var(--input-padding) + 8px) !important;
|
||||
background-color: var(--background-page);
|
||||
border: var(--border-width) solid var(--border-normal);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--border-normal-alt);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: var(--family-monospace);
|
||||
}
|
||||
|
||||
.serif {
|
||||
font-family: var(--family-serif);
|
||||
}
|
||||
|
||||
.sans-serif {
|
||||
font-family: var(--family-sans-serif);
|
||||
}
|
||||
|
||||
.uploader-drawer-content {
|
||||
padding: var(--content-padding);
|
||||
padding-top: 0;
|
||||
padding-bottom: var(--content-padding);
|
||||
}
|
||||
|
||||
.uploader-preview-image {
|
||||
margin-bottom: var(--form-vertical-gap);
|
||||
background-color: var(--background-normal);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.uploader-preview-image img {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 40vh;
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
160
app/src/interfaces/input-block-editor/plugins.ts
Normal file
160
app/src/interfaces/input-block-editor/plugins.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import BaseAttachesTool from '@editorjs/attaches';
|
||||
import BaseImageTool from '@editorjs/image';
|
||||
import { unexpectedError } from '@/utils/unexpected-error';
|
||||
|
||||
/**
|
||||
* This file is a modified version of the attaches and image tool from editorjs to work with the Directus file manager.
|
||||
*
|
||||
* We include an uploader to directly use Directus file manager, along with a modified version of the attaches and image tools.
|
||||
*/
|
||||
|
||||
class Uploader {
|
||||
getCurrentFile: any;
|
||||
config: any;
|
||||
onUpload: any;
|
||||
onError: any;
|
||||
constructor({
|
||||
config,
|
||||
getCurrentFile,
|
||||
onUpload,
|
||||
onError,
|
||||
}: {
|
||||
config: any;
|
||||
getCurrentFile?: any;
|
||||
onUpload: any;
|
||||
onError: any;
|
||||
}) {
|
||||
this.getCurrentFile = getCurrentFile;
|
||||
this.config = config;
|
||||
this.onUpload = onUpload;
|
||||
this.onError = onError;
|
||||
}
|
||||
|
||||
async uploadByFile(file: any, { onPreview }: any) {
|
||||
try {
|
||||
await Promise.all([this.uploadSelectedFile({ onPreview }), onPreview()]);
|
||||
|
||||
if (!this.config.uploader.getUploadFieldElement) return;
|
||||
|
||||
this.config.uploader.getUploadFieldElement().onBrowseSelect({
|
||||
target: {
|
||||
files: [file],
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
unexpectedError(err);
|
||||
}
|
||||
}
|
||||
|
||||
uploadByUrl(url: string) {
|
||||
this.onUpload({
|
||||
success: 1,
|
||||
file: {
|
||||
url: url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
uploadSelectedFile({ onPreview }: { onPreview: any }) {
|
||||
if (this.getCurrentFile) {
|
||||
const currentPreview = this.getCurrentFile();
|
||||
|
||||
if (currentPreview) {
|
||||
this.config.uploader.setCurrentPreview(
|
||||
this.config.uploader.addTokenToURL(currentPreview) + '&key=system-large-contain'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.config.uploader.setFileHandler(
|
||||
(file: { width: any; height: any; filesize: any; filename_download: string; title: any; id: string }) => {
|
||||
if (!file) {
|
||||
this.onError({
|
||||
success: 0,
|
||||
message: this.config.t.no_file_selected,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const response = {
|
||||
success: 1,
|
||||
file: {
|
||||
width: file.width,
|
||||
height: file.height,
|
||||
size: file.filesize,
|
||||
name: file.filename_download,
|
||||
title: file.title,
|
||||
extension: file.filename_download.split('.').pop(),
|
||||
fileId: file.id,
|
||||
fileURL: this.config.uploader.baseURL + 'files/' + file.id,
|
||||
url: this.config.uploader.baseURL + 'assets/' + file.id,
|
||||
},
|
||||
};
|
||||
|
||||
onPreview(this.config.uploader.addTokenToURL(response.file.fileURL));
|
||||
this.onUpload(response);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class AttachesTool extends BaseAttachesTool {
|
||||
constructor(params: {
|
||||
config: { uploader: any };
|
||||
block: { save: () => Promise<any> };
|
||||
api: { blocks: { update: (arg0: any, arg1: any) => void } };
|
||||
}) {
|
||||
super(params);
|
||||
|
||||
this.config.uploader = params.config.uploader;
|
||||
|
||||
this.uploader = new Uploader({
|
||||
config: this.config,
|
||||
onUpload: (response: any) => this.onUpload(response),
|
||||
onError: (error: any) => this.uploadingFailed(error),
|
||||
});
|
||||
|
||||
this.onUpload = (response: any) => {
|
||||
super.onUpload(response);
|
||||
|
||||
params.block.save().then((state) => {
|
||||
params.api.blocks.update(state.id, state.data);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
showFileData() {
|
||||
super.showFileData();
|
||||
|
||||
if (this.data.file && this.data.file.url) {
|
||||
const downloadButton = this.nodes.wrapper.querySelector('a.cdx-attaches__download-button');
|
||||
|
||||
if (downloadButton) {
|
||||
downloadButton.href = this.config.uploader.addTokenToURL(this.data.file.url) + '&download';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ImageTool extends BaseImageTool {
|
||||
constructor(params: any) {
|
||||
super(params);
|
||||
|
||||
this.uploader = new Uploader({
|
||||
config: this.config,
|
||||
getCurrentFile: () => this.data?.file?.url,
|
||||
onUpload: (response: any) => this.onUpload(response),
|
||||
onError: (error: any) => this.uploadingFailed(error),
|
||||
});
|
||||
}
|
||||
|
||||
set image(file: { url?: any }) {
|
||||
this._data.file = file || {};
|
||||
|
||||
if (file && file.url) {
|
||||
const imageUrl = this.config.uploader.addTokenToURL(file.url) + '&key=system-large-contain';
|
||||
this.ui.fillImage(imageUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
app/src/interfaces/input-block-editor/preview.svg
Normal file
14
app/src/interfaces/input-block-editor/preview.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="156" height="96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="18" y="15" width="120" height="66" rx="6" fill="var(--background-page)" class="glow" />
|
||||
<rect x="19" y="16" width="118" height="64" rx="5" stroke="var(--primary)" stroke-width="2" />
|
||||
<rect x="28" y="25" width="6" height="6" rx="2" fill="var(--primary)" fill-opacity=".25" />
|
||||
<rect x="28" y="45.148" width="6" height="6" rx="2" fill="var(--primary)" fill-opacity=".25" />
|
||||
<rect x="28" y="55.222" width="6" height="6" rx="2" fill="var(--primary)" fill-opacity=".25" />
|
||||
<rect x="28" y="65.296" width="6" height="6" rx="2" fill="var(--primary)" fill-opacity=".25" />
|
||||
<rect x="28" y="35.074" width="6" height="6" rx="2" fill="var(--primary)" fill-opacity=".25" />
|
||||
<rect x="42" y="25" width="10" height="6" rx="2" fill="var(--primary)" />
|
||||
<rect x="46" y="35" width="50" height="6" rx="2" fill="var(--primary)" fill-opacity=".25" />
|
||||
<rect x="46" y="45" width="60" height="6" rx="2" fill="var(--primary)" fill-opacity=".25" />
|
||||
<rect x="46" y="55" width="40" height="6" rx="2" fill="var(--primary)" fill-opacity=".25" />
|
||||
<rect x="42" y="65" width="10" height="6" rx="2" fill="var(--primary)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
127
app/src/interfaces/input-block-editor/tools.ts
Normal file
127
app/src/interfaces/input-block-editor/tools.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import ChecklistTool from '@editorjs/checklist';
|
||||
import CodeTool from '@editorjs/code';
|
||||
import DelimiterTool from '@editorjs/delimiter';
|
||||
import EmbedTool from '@editorjs/embed';
|
||||
import HeaderTool from '@editorjs/header';
|
||||
import InlineCodeTool from '@editorjs/inline-code';
|
||||
import NestedListTool from '@editorjs/nested-list';
|
||||
import ParagraphTool from '@editorjs/paragraph';
|
||||
import QuoteTool from '@editorjs/quote';
|
||||
import RawToolTool from '@editorjs/raw';
|
||||
import TableTool from '@editorjs/table';
|
||||
import UnderlineTool from '@editorjs/underline';
|
||||
import AlignmentTuneTool from 'editorjs-text-alignment-blocktune';
|
||||
import ToggleBlock from 'editorjs-toggle-block';
|
||||
import { AttachesTool, ImageTool } from './plugins';
|
||||
|
||||
export type UploaderConfig = {
|
||||
addTokenToURL: (url: string, token: string) => string;
|
||||
baseURL: string | undefined;
|
||||
setFileHandler: (handler: any) => void;
|
||||
setCurrentPreview?: (url: string) => void;
|
||||
getUploadFieldElement: () => any;
|
||||
};
|
||||
|
||||
export default function getTools(
|
||||
uploaderConfig: UploaderConfig,
|
||||
selection: Array<string>,
|
||||
haveFilesAccess: boolean
|
||||
): Record<string, object> {
|
||||
const tools: Record<string, any> = {};
|
||||
const fileRequiresTools = ['attaches', 'image'];
|
||||
|
||||
const defaults: Record<string, any> = {
|
||||
header: {
|
||||
class: HeaderTool,
|
||||
inlineToolbar: true,
|
||||
},
|
||||
list: {
|
||||
class: NestedListTool,
|
||||
inlineToolbar: false,
|
||||
},
|
||||
nestedlist: {
|
||||
class: NestedListTool,
|
||||
inlineToolbar: true,
|
||||
},
|
||||
embed: {
|
||||
class: EmbedTool,
|
||||
inlineToolbar: true,
|
||||
},
|
||||
paragraph: {
|
||||
class: ParagraphTool,
|
||||
inlineToolbar: true,
|
||||
},
|
||||
code: {
|
||||
class: CodeTool,
|
||||
},
|
||||
underline: {
|
||||
class: UnderlineTool,
|
||||
},
|
||||
table: {
|
||||
class: TableTool,
|
||||
inlineToolbar: true,
|
||||
},
|
||||
quote: {
|
||||
class: QuoteTool,
|
||||
inlineToolbar: true,
|
||||
},
|
||||
inlinecode: {
|
||||
class: InlineCodeTool,
|
||||
},
|
||||
delimiter: {
|
||||
class: DelimiterTool,
|
||||
},
|
||||
raw: {
|
||||
class: RawToolTool,
|
||||
},
|
||||
checklist: {
|
||||
class: ChecklistTool,
|
||||
inlineToolbar: true,
|
||||
},
|
||||
image: {
|
||||
class: ImageTool,
|
||||
config: {
|
||||
uploader: uploaderConfig,
|
||||
},
|
||||
},
|
||||
attaches: {
|
||||
class: AttachesTool,
|
||||
config: {
|
||||
uploader: uploaderConfig,
|
||||
},
|
||||
},
|
||||
toggle: {
|
||||
class: ToggleBlock,
|
||||
inlineToolbar: true,
|
||||
},
|
||||
alignment: {
|
||||
class: AlignmentTuneTool,
|
||||
},
|
||||
};
|
||||
|
||||
for (const toolName of selection) {
|
||||
if (!haveFilesAccess && fileRequiresTools.includes(toolName)) continue;
|
||||
|
||||
if (toolName in defaults) {
|
||||
tools[toolName] = defaults[toolName];
|
||||
}
|
||||
}
|
||||
|
||||
// Add alignment to all tools that support it if it's enabled.
|
||||
// editor.js tools means we need to already declare alignment in the tools object before we can assign it to other tools.
|
||||
if ('alignment' in tools) {
|
||||
if ('paragraph' in tools) {
|
||||
tools['paragraph'].tunes = ['alignment'];
|
||||
}
|
||||
|
||||
if ('header' in tools) {
|
||||
tools['header'].tunes = ['alignment'];
|
||||
}
|
||||
|
||||
if ('quote' in tools) {
|
||||
tools['quote'].tunes = ['alignment'];
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
38
app/src/interfaces/input-block-editor/use-file-handler.ts
Normal file
38
app/src/interfaces/input-block-editor/use-file-handler.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
type UploaderHandler = (file: Record<string, any>) => void;
|
||||
|
||||
export function useFileHandler() {
|
||||
const fileHandler = ref<UploaderHandler | null>(null);
|
||||
const currentPreview = ref<string | null>(null);
|
||||
|
||||
return {
|
||||
fileHandler,
|
||||
unsetFileHandler,
|
||||
setFileHandler,
|
||||
handleFile,
|
||||
currentPreview,
|
||||
setCurrentPreview,
|
||||
};
|
||||
|
||||
function setCurrentPreview(url: string | undefined | null) {
|
||||
currentPreview.value = url || null;
|
||||
}
|
||||
|
||||
function unsetFileHandler() {
|
||||
fileHandler.value = null;
|
||||
currentPreview.value = null;
|
||||
}
|
||||
|
||||
function setFileHandler(handler: UploaderHandler) {
|
||||
fileHandler.value = handler;
|
||||
}
|
||||
|
||||
function handleFile(event: InputEvent | null) {
|
||||
if (fileHandler.value && event) {
|
||||
fileHandler.value(event);
|
||||
}
|
||||
|
||||
unsetFileHandler();
|
||||
}
|
||||
}
|
||||
@@ -1764,6 +1764,27 @@ interfaces:
|
||||
folder_note: Folder for uploaded files. Does not affect existing files.
|
||||
imageToken: Static Access Token
|
||||
imageToken_label: Static access token is appended to the assets' URL
|
||||
input-block-editor:
|
||||
input-block-editor: Block Editor
|
||||
description: Block-styled editor for rich media stories, outputs clean data in JSON using Editor.js
|
||||
tools: Toolbar
|
||||
tools_options:
|
||||
header: Header
|
||||
nestedlist: List
|
||||
embed: Embed
|
||||
paragraph: Paragraph
|
||||
code: Code
|
||||
image: Image
|
||||
attaches: Attaches
|
||||
table: Table
|
||||
quote: Quote
|
||||
underline: Underline
|
||||
inlinecode: Inline Code
|
||||
delimiter: Delimiter
|
||||
checklist: Checklist
|
||||
toggle: Toggle Block
|
||||
alignment: Alignment
|
||||
raw: Raw HTML
|
||||
input-autocomplete-api:
|
||||
input-autocomplete-api: Autocomplete Input (API)
|
||||
description: A search typeahead for external API values.
|
||||
|
||||
17
app/src/shims.d.ts
vendored
17
app/src/shims.d.ts
vendored
@@ -34,3 +34,20 @@ declare module 'frappe-charts/src/js/charts/AxisChart' {
|
||||
}
|
||||
|
||||
declare module '@directus-extensions' {}
|
||||
|
||||
declare module '@editorjs/image';
|
||||
declare module '@editorjs/attaches';
|
||||
declare module '@editorjs/paragraph';
|
||||
declare module '@editorjs/quote';
|
||||
declare module '@editorjs/checklist';
|
||||
declare module '@editorjs/delimiter';
|
||||
declare module '@editorjs/table';
|
||||
declare module '@editorjs/code';
|
||||
declare module '@editorjs/header';
|
||||
declare module '@editorjs/underline';
|
||||
declare module '@editorjs/embed';
|
||||
declare module '@editorjs/raw';
|
||||
declare module '@editorjs/inline-code';
|
||||
declare module '@editorjs/nested-list';
|
||||
declare module 'editorjs-text-alignment-blocktune';
|
||||
declare module 'editorjs-toggle-block';
|
||||
|
||||
189
pnpm-lock.yaml
generated
189
pnpm-lock.yaml
generated
@@ -517,6 +517,51 @@ importers:
|
||||
'@directus/utils':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/utils
|
||||
'@editorjs/attaches':
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0
|
||||
'@editorjs/checklist':
|
||||
specifier: 1.5.0
|
||||
version: 1.5.0
|
||||
'@editorjs/code':
|
||||
specifier: 2.8.0
|
||||
version: 2.8.0
|
||||
'@editorjs/delimiter':
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0
|
||||
'@editorjs/editorjs':
|
||||
specifier: 2.26.5
|
||||
version: 2.26.5
|
||||
'@editorjs/embed':
|
||||
specifier: 2.5.3
|
||||
version: 2.5.3
|
||||
'@editorjs/header':
|
||||
specifier: 2.7.0
|
||||
version: 2.7.0
|
||||
'@editorjs/image':
|
||||
specifier: 2.8.1
|
||||
version: 2.8.1
|
||||
'@editorjs/inline-code':
|
||||
specifier: 1.4.0
|
||||
version: 1.4.0
|
||||
'@editorjs/nested-list':
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0
|
||||
'@editorjs/paragraph':
|
||||
specifier: 2.9.0
|
||||
version: 2.9.0
|
||||
'@editorjs/quote':
|
||||
specifier: 2.5.0
|
||||
version: 2.5.0
|
||||
'@editorjs/raw':
|
||||
specifier: 2.4.0
|
||||
version: 2.4.0
|
||||
'@editorjs/table':
|
||||
specifier: 2.2.1
|
||||
version: 2.2.1
|
||||
'@editorjs/underline':
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0
|
||||
'@fortawesome/fontawesome-svg-core':
|
||||
specifier: 6.4.0
|
||||
version: 6.4.0
|
||||
@@ -706,6 +751,12 @@ importers:
|
||||
dompurify:
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2
|
||||
editorjs-text-alignment-blocktune:
|
||||
specifier: 1.0.3
|
||||
version: 1.0.3
|
||||
editorjs-toggle-block:
|
||||
specifier: 0.3.11
|
||||
version: 0.3.11
|
||||
escape-string-regexp:
|
||||
specifier: 5.0.0
|
||||
version: 5.0.0
|
||||
@@ -4355,6 +4406,30 @@ packages:
|
||||
prettier: 2.8.7
|
||||
dev: true
|
||||
|
||||
/@codexteam/icons@0.0.2:
|
||||
resolution: {integrity: sha512-KdeKj3TwaTHqM3IXd5YjeJP39PBUZTb+dtHjGlf5+b0VgsxYD4qzsZkb11lzopZbAuDsHaZJmAYQ8LFligIT6Q==}
|
||||
dev: true
|
||||
|
||||
/@codexteam/icons@0.0.4:
|
||||
resolution: {integrity: sha512-V8N/TY2TGyas4wLrPIFq7bcow68b3gu8DfDt1+rrHPtXxcexadKauRJL6eQgfG7Z0LCrN4boLRawR4S9gjIh/Q==}
|
||||
dev: true
|
||||
|
||||
/@codexteam/icons@0.0.5:
|
||||
resolution: {integrity: sha512-s6H2KXhLz2rgbMZSkRm8dsMJvyUNZsEjxobBEg9ztdrb1B2H3pEzY6iTwI4XUPJWJ3c3qRKwV4TrO3J5jUdoQA==}
|
||||
dev: true
|
||||
|
||||
/@codexteam/icons@0.0.6:
|
||||
resolution: {integrity: sha512-L7Q5PET8PjKcBT5wp7VR+FCjwCi5PUp7rd/XjsgQ0CI5FJz0DphyHGRILMuDUdCW2MQT9NHbVr4QP31vwAkS/A==}
|
||||
dev: true
|
||||
|
||||
/@codexteam/icons@0.1.0:
|
||||
resolution: {integrity: sha512-jW1fWnwtWzcP4FBGsaodbJY3s1ZaRU+IJy1pvJ7ygNQxkQinybJcwXoyt0a5mWwu/4w30A42EWhCrZn8lp4fdw==}
|
||||
dev: true
|
||||
|
||||
/@codexteam/icons@0.3.0:
|
||||
resolution: {integrity: sha512-fJE9dfFdgq8xU+sbsxjH0Kt8Yeatw9xHBJWb77DhRkEXz3OCoIS6hrRC1ewHEryxzIjxD8IyQrRq2f+Gz3BcmA==}
|
||||
dev: true
|
||||
|
||||
/@colors/colors@1.5.0:
|
||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
@@ -4413,6 +4488,98 @@ packages:
|
||||
- '@algolia/client-search'
|
||||
dev: true
|
||||
|
||||
/@editorjs/attaches@1.3.0:
|
||||
resolution: {integrity: sha512-D+dj55EsC5bvXV///Dh/+KN4unuI0j6SU3f5QcMA5zrfda47PNKwbKOuwMBJwn0O+o5qwqrFv67g98lAo2qqVQ==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.4
|
||||
dev: true
|
||||
|
||||
/@editorjs/checklist@1.5.0:
|
||||
resolution: {integrity: sha512-2QUKDvLmtOFDaQLX6C+D9A4hIGBhsYwZW+60fOghSwC80dD931zOAA71cf+xppCTEhcr4eT18C6BJkBGf/DjTw==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.3.0
|
||||
dev: true
|
||||
|
||||
/@editorjs/code@2.8.0:
|
||||
resolution: {integrity: sha512-qlv1OqSEPKnLv/ZQgNFmVgqsMbGDkh/qgTEnbVWsS+yujo1nlwgVkqCB8tOkQGVsrpmAYLiWRlA413nKxxCN5w==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.5
|
||||
dev: true
|
||||
|
||||
/@editorjs/delimiter@1.3.0:
|
||||
resolution: {integrity: sha512-/qz+3yRSPmx6dJpBcwnDfgKtCjOrCsQRoDhXR+wLWirGOchOifhYwQ/6JZyTvcgmY84UEoYY29/dgdWOGVPEEQ==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.5
|
||||
dev: true
|
||||
|
||||
/@editorjs/editorjs@2.26.5:
|
||||
resolution: {integrity: sha512-imwXZi9NmzxKjNosa1xQf286liJYsTe2J2DWCiV5TwKhvYZ1INg5Y+FietcM2v65QmeLqP7wgBUhoI7wiCB+yQ==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.1.0
|
||||
codex-notifier: 1.1.2
|
||||
codex-tooltip: 1.0.5
|
||||
html-janitor: 2.0.4
|
||||
nanoid: 3.3.6
|
||||
dev: true
|
||||
|
||||
/@editorjs/embed@2.5.3:
|
||||
resolution: {integrity: sha512-+X0xX2tiwQjU/B2rPDv8DOwg0suiqrt2h/o9frjR308YuY1VTpqjLRF1lIV4TmelQyVr9cXkZruaa4Ty1wvfJg==}
|
||||
dev: true
|
||||
|
||||
/@editorjs/header@2.7.0:
|
||||
resolution: {integrity: sha512-4fGKGe2ZYblVqR/P/iw5ieG00uXInFgNMftBMqJRYcB2hUPD30kuu7Sn6eJDcLXoKUMOeqi8Z2AlUxYAmvw7zQ==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.5
|
||||
dev: true
|
||||
|
||||
/@editorjs/image@2.8.1:
|
||||
resolution: {integrity: sha512-4WscDAoi6OO0F6L7N1mkQymADwj8hHgH/ICk5wGRPdkesUZW1TgldX8XvSmy+f5VylsEi3F/gUggaZsrYxu2sA==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.6
|
||||
dev: true
|
||||
|
||||
/@editorjs/inline-code@1.4.0:
|
||||
resolution: {integrity: sha512-nJJx2eBgQyml7U8MdMdJNFY2RgZCOuvvXHEW73xsdu36ZXCd44eAo7vq1S5Jz9l8bC676SvNbRfeH/nojXK37A==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.5
|
||||
dev: true
|
||||
|
||||
/@editorjs/nested-list@1.3.0:
|
||||
resolution: {integrity: sha512-diRsa64YjR4Xnf5jbSxnRn9kfFc0ZOQBqp0dx3ZwHkm5wxN9D46KI7qXdIyOoUNzAFyK+/Q5c29PcoBJLOp01w==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.2
|
||||
dev: true
|
||||
|
||||
/@editorjs/paragraph@2.9.0:
|
||||
resolution: {integrity: sha512-lI6x1eiqFx2X3KmMak6gBlimFqXhG35fQpXMxzrjIphNLr4uPsXhVbyMPimPoLUnS9rM6Q00vptXmP2QNDd0zg==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.4
|
||||
dev: true
|
||||
|
||||
/@editorjs/quote@2.5.0:
|
||||
resolution: {integrity: sha512-24Mu8cESaj34a0kg1Enj7qiZ3yiCOsZI59+8xpfXLO/NkO7hBYWNForVcBy5yIWs/VLlEZK11FP37f/mHrKugQ==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.5
|
||||
dev: true
|
||||
|
||||
/@editorjs/raw@2.4.0:
|
||||
resolution: {integrity: sha512-6k7ngx1T8+ztTG4/i5QcGPKXkF2YvdqgKgtzpOTaG6Pzm17D7Hr30Krbqz2E2Y/uoV8SiR/X1UAyKTQxrk9B6Q==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.4
|
||||
dev: true
|
||||
|
||||
/@editorjs/table@2.2.1:
|
||||
resolution: {integrity: sha512-6RJKF0DfPh7YuBVPmT5mHXjjhil2IJ8ytZpw2hTHAuduqoRlu6OJbaGzH2NTCdg7cgD4H4BbX0RB9kcVxRtB2w==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.6
|
||||
dev: true
|
||||
|
||||
/@editorjs/underline@1.1.0:
|
||||
resolution: {integrity: sha512-vQj2ROW1KreD31QHlhaPikmDJGWYzRBusN4Zyfwl9nIIQCByt4S8fZQpsrRvH4sct5mkirsHllNT00rJlqHK7Q==}
|
||||
dependencies:
|
||||
'@codexteam/icons': 0.0.6
|
||||
dev: true
|
||||
|
||||
/@emotion/use-insertion-effect-with-fallbacks@1.0.0(react@18.0.0):
|
||||
resolution: {integrity: sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==}
|
||||
peerDependencies:
|
||||
@@ -9082,6 +9249,14 @@ packages:
|
||||
resolution: {integrity: sha512-z2jlHBocElRnPYysN2HAuhXbO3DNB0bcSKmNz3hcWR2Js2Dkhc1bEOxG93Z3DeUrnm+qx56XOY5wQmbP5KY0sw==}
|
||||
dev: true
|
||||
|
||||
/codex-notifier@1.1.2:
|
||||
resolution: {integrity: sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==}
|
||||
dev: true
|
||||
|
||||
/codex-tooltip@1.0.5:
|
||||
resolution: {integrity: sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag==}
|
||||
dev: true
|
||||
|
||||
/collect-v8-coverage@1.0.1:
|
||||
resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
|
||||
dev: true
|
||||
@@ -10129,6 +10304,16 @@ packages:
|
||||
sigmund: 1.0.1
|
||||
dev: true
|
||||
|
||||
/editorjs-text-alignment-blocktune@1.0.3:
|
||||
resolution: {integrity: sha512-DAJ2LJP+WjETvU4lVOJCqZ1kZkKHp0GkujkqZgza9DRv7bO46m1M/s2d5Ba5hPFdmqk1vA/ci++MQXgWD7mWYw==}
|
||||
dev: true
|
||||
|
||||
/editorjs-toggle-block@0.3.11:
|
||||
resolution: {integrity: sha512-Z0/0/OKofiPU5rxlNcz7U/VANK0PGMWyKQ2tVZytMaoyHmqZXmpa68xKATvxO9ldher6873wjgOWT6ZK1bi5YA==}
|
||||
dependencies:
|
||||
uuid: 9.0.0
|
||||
dev: true
|
||||
|
||||
/ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
@@ -11691,6 +11876,10 @@ packages:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
dev: true
|
||||
|
||||
/html-janitor@2.0.4:
|
||||
resolution: {integrity: sha512-92J5h9jNZRk30PMHapjHEJfkrBWKCOy0bq3oW2pBungky6lzYSoboBGPMvxl1XRKB2q+kniQmsLsPbdpY7RM2g==}
|
||||
dev: true
|
||||
|
||||
/htmlparser2@8.0.2:
|
||||
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user