This commit is contained in:
2023-07-10 16:05:00 +10:00
parent 2cb9aac070
commit 14aa5e3b28
10 changed files with 804 additions and 259 deletions

74
package-lock.json generated
View File

@@ -5,6 +5,11 @@
"packages": {
"": {
"dependencies": {
"@tiptap/extension-highlight": "^2.0.3",
"@tiptap/extension-image": "^2.0.3",
"@tiptap/extension-link": "^2.0.3",
"@tiptap/extension-subscript": "^2.0.3",
"@tiptap/extension-superscript": "^2.0.3",
"@tiptap/extension-text-align": "^2.0.3",
"@tiptap/extension-underline": "^2.0.3",
"@tiptap/pm": "^2.0.3",
@@ -1536,6 +1541,18 @@
"@tiptap/core": "^2.0.0"
}
},
"node_modules/@tiptap/extension-highlight": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.0.3.tgz",
"integrity": "sha512-NrtibY8cZkIjZMQuHRrKd4php+plOvAoSo8g3uVFu275I/Ixt5HqJ53R4voCXs8W8BOBRs2HS2QX8Cjh79XhtA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.0.0"
}
},
"node_modules/@tiptap/extension-history": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.0.3.tgz",
@@ -1562,6 +1579,18 @@
"@tiptap/pm": "^2.0.0"
}
},
"node_modules/@tiptap/extension-image": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.0.3.tgz",
"integrity": "sha512-hS9ZJwz0md07EHsC+o4NuuJkhCZsZn7TuRz/2CvRSj2fWFIz+40CyNAHf/2J0qNugG9ommXaemetsADeEZP9ag==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.0.0"
}
},
"node_modules/@tiptap/extension-italic": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.0.3.tgz",
@@ -1574,6 +1603,22 @@
"@tiptap/core": "^2.0.0"
}
},
"node_modules/@tiptap/extension-link": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.0.3.tgz",
"integrity": "sha512-H72tXQ5rkVCkAhFaf08fbEU7EBUCK0uocsqOF+4th9sOlrhfgyJtc8Jv5EXPDpxNgG5jixSqWBo0zKXQm9s9eg==",
"dependencies": {
"linkifyjs": "^4.1.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.0.0",
"@tiptap/pm": "^2.0.0"
}
},
"node_modules/@tiptap/extension-list-item": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.0.3.tgz",
@@ -1622,6 +1667,30 @@
"@tiptap/core": "^2.0.0"
}
},
"node_modules/@tiptap/extension-subscript": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.0.3.tgz",
"integrity": "sha512-XFAEUaKxWRmTq7ePEF4aj7knelJPr2fTz0y/iSXydtS094LKwBHBzxatIZY3phrgfpDc+f51ycwarsgz27UJfg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.0.0"
}
},
"node_modules/@tiptap/extension-superscript": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.0.3.tgz",
"integrity": "sha512-5EBjUvkw2SXL1e8C1i0UF26/GBNHxEbiNQKw7Shy88omVa4HTY+D8KWC/j29ZW/IomUbGPlbpXp1z+1TETzmyw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^2.0.0"
}
},
"node_modules/@tiptap/extension-text": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.3.tgz",
@@ -4465,6 +4534,11 @@
"uc.micro": "^1.0.1"
}
},
"node_modules/linkifyjs": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.1.tgz",
"integrity": "sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA=="
},
"node_modules/local-pkg": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz",

View File

@@ -31,6 +31,11 @@
"vitest": "^0.32.0"
},
"dependencies": {
"@tiptap/extension-highlight": "^2.0.3",
"@tiptap/extension-image": "^2.0.3",
"@tiptap/extension-link": "^2.0.3",
"@tiptap/extension-subscript": "^2.0.3",
"@tiptap/extension-superscript": "^2.0.3",
"@tiptap/extension-text-align": "^2.0.3",
"@tiptap/extension-underline": "^2.0.3",
"@tiptap/pm": "^2.0.3",

View File

@@ -2,21 +2,8 @@
<div class="sm-html">
<div
v-if="editor"
class="flex bg-white border-t border-x border-gray rounded-t-2">
<button
@click.prevent="editor.chain().focus().toggleInfo().run()"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive('info')
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
III
</button>
<div class="flex px-1 border-r border-gray relative">
class="flex flex-wrap bg-white border border-gray rounded-t-2">
<div class="flex px-1 relative border-r">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960"
@@ -63,9 +50,27 @@
:selected="editor.isActive('heading', { level: 6 })">
Heading 6
</option>
<option value="info" :selected="editor.isActive('info')">
Info
</option>
<option
value="success"
:selected="editor.isActive('success')">
Success
</option>
<option
value="warning"
:selected="editor.isActive('warning')">
Warning
</option>
<option
value="danger"
:selected="editor.isActive('danger')">
Danger
</option>
</select>
</div>
<div class="flex p-1 border-r border-gray">
<div class="flex p-1 border-r">
<button
@click.prevent="editor.chain().focus().toggleBold().run()"
:disabled="!editor.can().chain().focus().toggleBold().run()"
@@ -133,7 +138,6 @@
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<title>format-underline</title>
<path
d="M5,21H19V19H5V21M12,17A6,6 0 0,0 18,11V3H15.5V11A3.5,3.5 0 0,1 12,14.5A3.5,3.5 0 0,1 8.5,11V3H6V11A6,6 0 0,0 12,17Z"
fill="currentColor" />
@@ -158,14 +162,220 @@
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<title>format-strikethrough-variant</title>
<path
d="M23,12V14H18.61C19.61,16.14 19.56,22 12.38,22C4.05,22.05 4.37,15.5 4.37,15.5L8.34,15.55C8.37,18.92 11.5,18.92 12.12,18.88C12.76,18.83 15.15,18.84 15.34,16.5C15.42,15.41 14.32,14.58 13.12,14H1V12H23M19.41,7.89L15.43,7.86C15.43,7.86 15.6,5.09 12.15,5.08C8.7,5.06 9,7.28 9,7.56C9.04,7.84 9.34,9.22 12,9.88H5.71C5.71,9.88 2.22,3.15 10.74,2C19.45,0.8 19.43,7.91 19.41,7.89Z"
fill="currentColor" />
</svg>
</button>
<button
@click.prevent="
editor.chain().focus().toggleHighlight().run()
"
:disabled="
!editor.can().chain().focus().toggleHighlight().run()
"
title="highlight"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive('highlight')
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<path
d="M18.5,1.15C17.97,1.15 17.46,1.34 17.07,1.73L11.26,7.55L16.91,13.2L22.73,7.39C23.5,6.61 23.5,5.35 22.73,4.56L19.89,1.73C19.5,1.34 19,1.15 18.5,1.15M10.3,8.5L4.34,14.46C3.56,15.24 3.56,16.5 4.36,17.31C3.14,18.54 1.9,19.77 0.67,21H6.33L7.19,20.14C7.97,20.9 9.22,20.89 10,20.12L15.95,14.16"
fill="currentColor" />
</svg>
</button>
</div>
<div class="flex p-1 border-r border-gray">
<div class="flex p-1 border-r">
<button
@click.prevent="
editor.chain().focus().setTextAlign('left').run()
"
title="align left"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive({ textAlign: 'left' })
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<path
d="M3,3H21V5H3V3M3,7H15V9H3V7M3,11H21V13H3V11M3,15H15V17H3V15M3,19H21V21H3V19Z"
fill="currentColor" />
</svg>
</button>
<button
@click.prevent="
editor.chain().focus().setTextAlign('center').run()
"
title="align center"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive({ textAlign: 'center' })
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<path
d="M3,3H21V5H3V3M7,7H17V9H7V7M3,11H21V13H3V11M7,15H17V17H7V15M3,19H21V21H3V19Z"
fill="currentColor" />
</svg>
</button>
<button
@click.prevent="
editor.chain().focus().setTextAlign('right').run()
"
title="align right"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive({ textAlign: 'right' })
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<path
d="M3,3H21V5H3V3M9,7H21V9H9V7M3,11H21V13H3V11M9,15H21V17H9V15M3,19H21V21H3V19Z"
fill="currentColor" />
</svg>
</button>
<button
@click.prevent="
editor.chain().focus().setTextAlign('justify').run()
"
title="align right"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive({ textAlign: 'justify' })
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<path
d="M3,3H21V5H3V3M3,7H21V9H3V7M3,11H21V13H3V11M3,15H21V17H3V15M3,19H21V21H3V19Z"
fill="currentColor" />
</svg>
</button>
</div>
<div class="flex p-1 border-r">
<button
@click.prevent="setLink()"
title="link"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive('link')
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<path
d="M3.9,12C3.9,10.29 5.29,8.9 7,8.9H11V7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H11V15.1H7C5.29,15.1 3.9,13.71 3.9,12M8,13H16V11H8V13M17,7H13V8.9H17C18.71,8.9 20.1,10.29 20.1,12C20.1,13.71 18.71,15.1 17,15.1H13V17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7Z"
fill="currentColor" />
</svg>
</button>
<button
@click.prevent="editor.chain().focus().unsetLink().run()"
title="unlink"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
'bg-white',
'text-gray-6',
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<path
d="M17,7H13V8.9H17C18.71,8.9 20.1,10.29 20.1,12C20.1,13.43 19.12,14.63 17.79,15L19.25,16.44C20.88,15.61 22,13.95 22,12A5,5 0 0,0 17,7M16,11H13.81L15.81,13H16V11M2,4.27L5.11,7.38C3.29,8.12 2,9.91 2,12A5,5 0 0,0 7,17H11V15.1H7C5.29,15.1 3.9,13.71 3.9,12C3.9,10.41 5.11,9.1 6.66,8.93L8.73,11H8V13H10.73L13,15.27V17H14.73L18.74,21L20,19.74L3.27,3L2,4.27Z"
fill="currentColor" />
</svg>
</button>
</div>
<div class="flex p-1 border-r">
<button
@click.prevent="setImage()"
title="image"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive('image')
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<path
d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M13.96,12.29L11.21,15.83L9.25,13.47L6.5,17H17.5L13.96,12.29Z"
fill="currentColor" />
</svg>
</button>
<button
@click.prevent="setLink()"
title="gallery"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive('gallery')
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<path
d="M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3M15.96,10.29L13.21,13.83L11.25,11.47L8.5,15H19.5L15.96,10.29Z"
fill="currentColor" />
</svg>
</button>
</div>
<div class="flex p-1 border-r">
<button
@click.prevent="
editor.chain().focus().toggleBulletList().run()
@@ -207,14 +417,13 @@
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-5"
viewBox="0 0 24 24">
<title>format-list-numbered</title>
<path
d="M7,13V11H21V13H7M7,19V17H21V19H7M7,7V5H21V7H7M3,8V5H2V4H4V8H3M2,17V16H5V20H2V19H4V18.5H3V17.5H4V17H2M4.25,10A0.75,0.75 0 0,1 5,10.75C5,10.95 4.92,11.14 4.79,11.27L3.12,13H5V14H2V13.08L4,11H2V10H4.25Z"
fill="currentColor" />
</svg>
</button>
</div>
<div class="flex p-1 border-r border-gray">
<div class="flex p-1 border-r">
<button
@click.prevent="
editor.chain().focus().toggleCodeBlock().run()
@@ -281,7 +490,57 @@
</svg>
</button>
</div>
<div class="flex p-1 border-r border-gray">
<div class="flex p-1 border-r">
<button
@click.prevent="
editor.chain().focus().unsetSuperscript().run();
editor.chain().focus().toggleSubscript().run();
"
title="subscript"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive('subscript')
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24">
<path
d="M16,7.41L11.41,12L16,16.59L14.59,18L10,13.41L5.41,18L4,16.59L8.59,12L4,7.41L5.41,6L10,10.59L14.59,6L16,7.41M21.85,21.03H16.97V20.03L17.86,19.23C18.62,18.58 19.18,18.04 19.56,17.6C19.93,17.16 20.12,16.75 20.13,16.36C20.14,16.08 20.05,15.85 19.86,15.66C19.68,15.5 19.39,15.38 19,15.38C18.69,15.38 18.42,15.44 18.16,15.56L17.5,15.94L17.05,14.77C17.32,14.56 17.64,14.38 18.03,14.24C18.42,14.1 18.85,14 19.32,14C20.1,14.04 20.7,14.25 21.1,14.66C21.5,15.07 21.72,15.59 21.72,16.23C21.71,16.79 21.53,17.31 21.18,17.78C20.84,18.25 20.42,18.7 19.91,19.14L19.27,19.66V19.68H21.85V21.03Z"
fill="currentColor" />
</svg>
</button>
<button
@click.prevent="
editor.chain().focus().unsetSubscript().run();
editor.chain().focus().toggleSuperscript().run();
"
title="Superscript"
:class="[
'flex',
'flex-items-center',
'p-1',
'hover-bg-gray-3',
editor.isActive('superscript')
? ['bg-sky-6', 'text-white']
: ['bg-white', 'text-gray-6'],
]">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24">
<path
d="M16,7.41L11.41,12L16,16.59L14.59,18L10,13.41L5.41,18L4,16.59L8.59,12L4,7.41L5.41,6L10,10.59L14.59,6L16,7.41M21.85,9H16.97V8L17.86,7.18C18.62,6.54 19.18,6 19.56,5.55C19.93,5.11 20.12,4.7 20.13,4.32C20.14,4.04 20.05,3.8 19.86,3.62C19.68,3.43 19.39,3.34 19,3.33C18.69,3.34 18.42,3.4 18.16,3.5L17.5,3.89L17.05,2.72C17.32,2.5 17.64,2.33 18.03,2.19C18.42,2.05 18.85,2 19.32,2C20.1,2 20.7,2.2 21.1,2.61C21.5,3 21.72,3.54 21.72,4.18C21.71,4.74 21.53,5.26 21.18,5.73C20.84,6.21 20.42,6.66 19.91,7.09L19.27,7.61V7.63H21.85V9Z"
fill="currentColor" />
</svg>
</button>
</div>
<div class="flex p-1 border-r">
<button
@click.prevent="editor.chain().focus().setHardBreak().run()"
title="hard break"
@@ -324,7 +583,7 @@
</svg>
</button>
</div>
<div class="flex p-1">
<div class="flex p-1 border-r">
<button
@click.prevent="editor.chain().focus().undo().run()"
title="Undo"
@@ -345,7 +604,6 @@
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24">
<title>undo</title>
<path
d="M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z"
fill="currentColor" />
@@ -371,7 +629,6 @@
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24">
<title>redo</title>
<path
d="M18.4,10.6C16.55,9 14.15,8 11.5,8C6.85,8 2.92,11.03 1.54,15.22L3.9,16C4.95,12.81 7.95,10.5 11.5,10.5C13.45,10.5 15.23,11.22 16.62,12.38L13,16H22V7L18.4,10.6Z"
fill="currentColor" />
@@ -381,7 +638,7 @@
</div>
<EditorContent
:editor="editor"
class="rounded-b-2 bg-white p-4 border-1 border-gray h-128 overflow-auto sm-editor" />
class="rounded-b-2 bg-white p-4 border-x border-b border-gray h-128 overflow-auto sm-editor" />
</div>
</template>
@@ -391,7 +648,18 @@ import { useEditor, EditorContent } from "@tiptap/vue-3";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline";
import TextAlign from "@tiptap/extension-text-align";
import Highlight from "@tiptap/extension-highlight";
import { Info } from "../extensions/info";
import { Success } from "../extensions/success";
import { Warning } from "../extensions/warning";
import { Danger } from "../extensions/danger";
import Subscript from "@tiptap/extension-subscript";
import Superscript from "@tiptap/extension-superscript";
import Link from "@tiptap/extension-link";
import Image from "@tiptap/extension-image";
import { openDialog } from "./SMDialog";
import SMDialogMedia from "./dialogs/SMDialogMedia.vue";
import { Media } from "../helpers/api.types";
const props = defineProps({
modelValue: {
@@ -404,7 +672,31 @@ const emits = defineEmits(["update:modelValue"]);
const editor = useEditor({
content: props.modelValue,
extensions: [StarterKit, Underline, TextAlign, Info],
extensions: [
StarterKit,
Underline,
TextAlign.configure({
types: [
"heading",
"paragraph",
"info",
"success",
"warning",
"danger",
],
}),
Highlight,
Info,
Success,
Warning,
Danger,
Subscript,
Superscript,
Link.configure({
openOnClick: false,
}),
Image,
],
onUpdate: () => {
emits("update:modelValue", editor.value.getHTML());
},
@@ -434,10 +726,60 @@ const updateNode = (event) => {
case "h6":
editor.value.chain().focus().setHeading({ level: 6 }).run();
break;
case "info":
editor.value.chain().focus().toggleInfo().run();
break;
case "success":
editor.value.chain().focus().toggleSuccess().run();
break;
case "warning":
editor.value.chain().focus().toggleWarning().run();
break;
case "danger":
editor.value.chain().focus().toggleDanger().run();
break;
}
}
};
const setLink = () => {
const previousUrl = editor.value.getAttributes("link").href;
const url = window.prompt("URL", previousUrl);
// cancelled
if (url === null) {
return;
}
// empty
if (url === "") {
editor.value.chain().focus().extendMarkRange("link").unsetLink().run();
return;
}
// update link
editor.value
.chain()
.focus()
.extendMarkRange("link")
.setLink({ href: url })
.run();
};
const setImage = async () => {
let result = await openDialog(SMDialogMedia);
if (result) {
const mediaResult = result as Media;
editor.value
.chain()
.focus()
.setImage({
src: mediaResult.url,
})
.run();
}
};
onBeforeUnmount(() => {
editor.value.destroy();
});

View File

@@ -18,6 +18,11 @@ defineProps({
type: String,
required: true,
},
hide: {
type: Boolean,
default: true,
required: false,
},
});
const selectedTab = inject("selectedTab");

View File

@@ -42,17 +42,22 @@ const emits = defineEmits(["tabChanged", "update:modelValue"]);
const slots = useSlots();
const tabs = ref(
slots.default().map((tab) => {
const { label, id } = tab.props;
slots
.default()
.map((tab) => {
const { label, id, hide } = tab.props;
if (hide !== true) {
return {
label,
id,
};
}
})
.filter(Boolean),
);
const selectedTab = ref(
props.modelValue.length == 0 ? tabs.value[0].id : props.modelValue
props.modelValue.length == 0 ? tabs.value[0].id : props.modelValue,
);
if (props.modelValue.length == 0) {
@@ -64,14 +69,14 @@ watch(
(newValue) => {
emits("tabChanged", newValue);
emits("update:modelValue", newValue);
}
},
);
watch(
() => props.modelValue,
(newValue) => {
selectedTab.value = newValue;
}
},
);
provide("selectedTab", selectedTab);

View File

@@ -1,28 +1,76 @@
<template>
<div
class="fixed top-0 left-0 w-full h-full z-2 bg-black bg-op-20 backdrop-blur"></div>
<div class="fixed top-0 left-0 w-full h-full flex-justify-center flex z-3">
<div
class="m-4 border-1 bg-white rounded-xl text-gray-5 px-12 py-8 w-full">
<div class="dialog-media">
class="fixed top-0 left-0 w-full flex-justify-center flex z-3 max-h-screen">
<div
class="m-4 border-1 bg-white rounded-xl text-gray-5 px-12 py-8 w-full overflow-auto">
<div>
<SMLoading v-if="progressText" overlay :text="progressText" />
<h2 class="mb-4">Insert Media</h2>
<SMTabGroup v-model="selectedTab">
<SMTab id="tab-browser" label="Media Browser">
<div class="flex mb-4">
<div
:buttons="[
{
name: 'grid',
icon: 'grid-outline',
},
{
name: 'list',
icon: 'list-outline',
},
<button
title="View as grid"
:class="[
'p-2',
'rounded-l-2',
'hover:shadow-md',
'transition',
'border-1',
'border-sky-600',
'cursor-pointer',
listActive != 'grid'
? [
'text-sky-600',
'bg-white',
'hover:bg-sky-500',
'hover:text-white',
]
: ['text-white', 'bg-sky-600'],
]"
:active="listActive"
@click="handleClickLayout"></div>
@click="handleClickLayout('grid')">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24">
<path
d="M3,11H11V3H3M3,21H11V13H3M13,21H21V13H13M13,3V11H21V3"
fill="currentColor" />
</svg>
</button>
<button
title="View as list"
:class="[
'p-2',
'rounded-r-2',
'hover:shadow-md',
'transition',
'hover:bg-sky-500',
'border-1',
'border-sky-600',
'cursor-pointer',
'mr-4',
listActive != 'list'
? [
'text-sky-600',
'bg-white',
'hover:bg-sky-500',
'hover:text-white',
]
: ['text-white', 'bg-sky-600'],
]"
@click="handleClickLayout('list')">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 24 24">
<path
d="M9,5V9H21V5M9,19H21V15H9M9,14H21V10H9M4,9H8V5H4M4,19H8V15H4M4,14H8V10H4V14Z"
fill="currentColor" />
</svg>
</button>
<SMInput
v-model="itemSearch"
label="Search"
@@ -47,40 +95,81 @@
</template>
</SMInput>
</div>
<div class="media-browser" :class="mediaBrowserClasses">
<div class="media-browser-content">
<div class="flex">
<div
class="flex flex-justify-center overflow-auto w-full">
<SMLoading v-if="mediaLoading" />
<div
v-if="
v-else-if="
!mediaLoading && mediaItems.length == 0
"
class="media-none">
<ion-icon name="sad-outline"></ion-icon>
<p>No media found</p>
class="py-12 text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960"
class="h-24 text-gray-5">
<path
d="M453-280h60v-240h-60v240Zm26.982-314q14.018 0 23.518-9.2T513-626q0-14.45-9.482-24.225-9.483-9.775-23.5-9.775-14.018 0-23.518 9.775T447-626q0 13.6 9.482 22.8 9.483 9.2 23.5 9.2Zm.284 514q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80-397.681 80-480.5q0-82.819 31.5-155.659Q143-709 197.5-763t127.341-85.5Q397.681-880 480.5-880q82.819 0 155.659 31.5Q709-817 763-763t85.5 127Q880-563 880-480.266q0 82.734-31.5 155.5T763-197.684q-54 54.316-127 86Q563-80 480.266-80Zm.234-60Q622-140 721-239.5t99-241Q820-622 721.188-721 622.375-820 480-820q-141 0-240.5 98.812Q140-622.375 140-480q0 141 99.5 240.5t241 99.5Zm-.5-340Z"
fill="currentColor" />
</svg>
<p class="text-lg text-gray-5">
No media found
</p>
</div>
<ul
v-if="
!mediaLoading && mediaItems.length > 0
">
"
:class="[
'flex',
'flex-1',
'gap-4',
'border-1',
'mb-4',
'p-3',
'overflow-auto',
'flex-justify-center',
listActive == 'grid'
? ['flex-row', 'flex-wrap']
: ['flex-col', 'flex-nowrap'],
]">
<li
v-for="item in mediaItems"
:key="item.id"
:class="[
{ selected: item.id == selected },
'flex',
'text-center',
'border-3',
'p-1px',
'flex-items-center',
listActive == 'grid'
? ['h-50', 'w-60', 'flex-col']
: ['ha', 'w-full', 'flex-row'],
item.id == selected
? 'border-sky-600'
: 'border-white',
]"
@click="handleClickItem(item.id)"
@dblclick="handleDblClickItem(item.id)">
<div
:class="[
listActive == 'grid'
? ['h-40', 'w-60', 'mr-0']
: ['h-20', 'w-20', 'mr-2'],
'bg-contain',
'bg-center',
'bg-no-repeat',
]"
:style="{
backgroundImage: `url('${mediaGetVariantUrl(
item,
'small'
'small',
)}')`,
}"
class="media-image"></div>
<span class="media-title">{{
item.title
}}</span>
}"></div>
<span
class="text-sm whitespace-nowrap overflow-hidden text-ellipsis block p-2"
>{{ item.title }}</span
>
</li>
</ul>
</div>
@@ -98,22 +187,26 @@
<SMForm v-model="uploadForm" form-id="upload-form">
<SMFormError v-model="uploadForm" />
<div class="flex">
<div width="250px">
<div class="text-center mr-4 w-60">
<div
class="upload-preview mb-4"
class="mb-4 h-34 border rounded-2 bg-cover bg-center bg-no-repeat"
:style="{
backgroundImage: `url(${uploadPreview})`,
backgroundImage:
uploadPreview.length > 0
? `url(${uploadPreview})`
: `url(\'${uploadPreviewMissing}\')`,
}"></div>
<button
type="button"
v-if="props.allowUpload"
class="font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
@click="handleClickSelectFile">
Select File
</button>
</div>
<div>
<div class="flex-1">
<SMInput
label="Title"
class="mb-4"
control="title"
form-id="upload-form"
:disabled="uploadPreview.length == 0" />
@@ -195,7 +288,7 @@ const props = defineProps({
},
allowUpload: {
type: Boolean,
default: true,
default: false,
required: false,
},
});
@@ -217,11 +310,13 @@ let uploadForm = reactive(
Form({
title: FormControl("", And([Required(), Min(4)])),
description: FormControl(""),
})
}),
);
const uploadPreview = ref("");
const uploadPreviewMissing = ref(
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="-20 -20 64 64"%3E%3Cpath d="M22 20.7L3.3 2L2 3.3L3 4.3V19C3 20.1 3.9 21 5 21H19.7L20.7 22L22 20.7M5 19V6.3L12.6 13.9L11.1 15.8L9 13.1L6 17H15.7L17.7 19H5M8.8 5L6.8 3H19C20.1 3 21 3.9 21 5V17.2L19 15.2V5H8.8" /%3E%3C/svg%3E',
);
/**
* Is the media loading/busy
*/
@@ -326,7 +421,7 @@ const handleClickInsert = async () => {
submitFormData.append("title", uploadForm.controls.title.value);
submitFormData.append(
"description",
uploadForm.controls.description.value
uploadForm.controls.description.value,
);
try {
let result = await api.post({
@@ -337,7 +432,8 @@ const handleClickInsert = async () => {
},
progress: (progressData) =>
(progressText.value = `Uploading File: ${Math.floor(
(progressData.loaded / progressData.total) * 100
(progressData.loaded / progressData.total) *
100,
)}%`),
});
if (result.data) {
@@ -357,7 +453,7 @@ const handleClickInsert = async () => {
"The server is taking longer then expected to process the file.\nOnce the file has been processed, select it from the media browser.";
} else {
await new Promise((resolve) =>
setTimeout(resolve, 500)
setTimeout(resolve, 500),
);
try {
let updateResult = await api.get({
@@ -377,7 +473,7 @@ const handleClickInsert = async () => {
mediaProcessed = true;
} else if (
updateData.medium.status.startsWith(
"Failed"
"Failed",
) == true
) {
throw "error";
@@ -481,7 +577,7 @@ const handleChangeSelectFile = async () => {
if (firstFile != null) {
if (uploadForm.controls.title.value.length == 0) {
uploadForm.controls.title.value = convertFileNameToTitle(
firstFile.name
firstFile.name,
);
}
@@ -595,131 +691,3 @@ const computedInsertDisabled = computed(() => {
handleLoad();
</script>
<style lang="scss">
.dialog-media {
width: 100%;
h3 {
margin-bottom: 16px;
}
.media-browser {
display: flex;
flex-direction: column;
.media-browser-content {
display: flex;
height: 40vh;
border: 1px solid var(--base-color-border);
background-color: var(--base-color-light);
justify-content: center;
align-items: center;
margin: 0 0 16px 0;
.media-none {
font-size: 150%;
text-align: center;
ion-icon {
font-size: 200%;
margin-bottom: 16px;
}
}
ul {
display: block;
list-style-type: none;
overflow: auto;
max-height: 40vh;
height: 100%;
width: 100%;
gap: 8px;
justify-content: center;
padding: 0;
li {
display: flex;
align-items: center;
border: 3px solid transparent;
box-sizing: content-box;
padding: 2px;
&.selected,
&:hover {
border-color: var(--primary-color);
}
.media-image {
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
}
}
}
&.media-browser-list {
ul {
flex-direction: column;
flex-wrap: nowrap;
}
li {
height: auto;
width: auto;
}
.media-image {
width: 64px;
height: 64px;
margin-right: 8px;
}
.media-title {
flex: 1;
text-align: left;
}
}
&.media-browser-grid {
ul {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
li {
flex-direction: column;
height: 160px;
width: 220px;
.media-image {
min-height: 132px;
min-width: 220px;
}
.media-title {
text-align: center;
padding: 4px;
font-size: 80%;
width: 224px;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.upload-preview {
width: 250px;
height: 140px;
border: 1px solid var(--base-color-dark);
border-radius: 8px;
background-position: center;
background-size: cover;
}
}
</style>

View File

@@ -0,0 +1,60 @@
import { mergeAttributes, Node } from "@tiptap/core";
export interface DangerOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
danger: {
/**
* Toggle a paragraph
*/
setDanger: () => ReturnType;
toggleDanger: () => ReturnType;
};
}
}
export const Danger = Node.create<DangerOptions>({
name: "danger",
priority: 1000,
addOptions() {
return {
HTMLAttributes: { class: "danger" },
};
},
group: "block",
content: "inline*",
parseHTML() {
return [{ tag: "p", class: "danger" }];
},
renderHTML({ HTMLAttributes }) {
return [
"p",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},
addCommands() {
return {
setDanger:
() =>
({ commands }) => {
return commands.setNode(this.name);
},
toggleDanger:
() =>
({ commands }) => {
return commands.toggleNode(this.name, "paragraph");
},
};
},
});

View File

@@ -0,0 +1,60 @@
import { mergeAttributes, Node } from "@tiptap/core";
export interface SuccessOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
success: {
/**
* Toggle a paragraph
*/
setSuccess: () => ReturnType;
toggleSuccess: () => ReturnType;
};
}
}
export const Success = Node.create<SuccessOptions>({
name: "success",
priority: 1000,
addOptions() {
return {
HTMLAttributes: { class: "success" },
};
},
group: "block",
content: "inline*",
parseHTML() {
return [{ tag: "p", class: "success" }];
},
renderHTML({ HTMLAttributes }) {
return [
"p",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},
addCommands() {
return {
setSuccess:
() =>
({ commands }) => {
return commands.setNode(this.name);
},
toggleSuccess:
() =>
({ commands }) => {
return commands.toggleNode(this.name, "paragraph");
},
};
},
});

View File

@@ -0,0 +1,60 @@
import { mergeAttributes, Node } from "@tiptap/core";
export interface WarningOptions {
HTMLAttributes: Record<string, any>;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
warning: {
/**
* Toggle a paragraph
*/
setWarning: () => ReturnType;
toggleWarning: () => ReturnType;
};
}
}
export const Warning = Node.create<WarningOptions>({
name: "warning",
priority: 1000,
addOptions() {
return {
HTMLAttributes: { class: "warning" },
};
},
group: "block",
content: "inline*",
parseHTML() {
return [{ tag: "p", class: "warning" }];
},
renderHTML({ HTMLAttributes }) {
return [
"p",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
},
addCommands() {
return {
setWarning:
() =>
({ commands }) => {
return commands.setNode(this.name);
},
toggleWarning:
() =>
({ commands }) => {
return commands.toggleNode(this.name, "paragraph");
},
};
},
});

View File

@@ -37,59 +37,25 @@
input { font-family: Poppins, Roboto, "Open Sans", ui-sans-serif, system-ui, sans-serif; }
.scrollbar-width-none { scrollbar-width: none; }
.scrollbar-width-none::-webkit-scrollbar { display: none; }
.bg-center { background-position: center; }
.whitespace-nowrap {white-space: nowrap; }
.spin{animation:rotate 1s infinite linear}
.sm-html .ProseMirror {
outline: none;
}
.sm-html hr {
border-top: 1px solid #aaa;
margin: 1.5rem 0;
}
.sm-html pre {
padding: 0 1rem;
line-height: 1rem;
}
.sm-html blockquote {
border-left: 4px solid #ddd;
margin-left: 1rem;
padding-left: 1rem;
}
.sm-html p.info {
display: flex;
border: 1px solid rgba(14,165,233,1);
background-color: rgba(14,165,233,0.25);
border-radius: 0.5rem;
padding: 0.5rem 1rem 0.5rem 0.75rem;
margin: 0.5rem;
font-size: 80%;
}
.sm-html p.info::before {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z' fill='currentColor' /%3E%3C/svg%3E");
display: inline-block;
color: rgba(14,165,233,1);
width: 1.5rem;
height: 1.5rem;
margin-right: 0.5rem;
margin-top: 0.1rem;
}
.sm-editor::-webkit-scrollbar {
background-color: transparent;
width: 16px;
}
.sm-editor::-webkit-scrollbar-thumb {
background-color: #aaa;
border: 4px solid transparent;
border-radius: 8px;
background-clip: padding-box;
}
.sm-html .ProseMirror { outline: none; }
.sm-html hr { border-top: 1px solid #aaa; margin: 1.5rem 0; }
.sm-html pre { padding: 0 1rem; line-height: 1rem; }
.sm-html blockquote { border-left: 4px solid #ddd; margin-left: 1rem; padding-left: 1rem; }
.sm-html p.info, .sm-html p.success, .sm-html p.warning, .sm-html p.danger { display: flex; border-radius: 0.5rem; padding: 0.5rem 1rem 0.5rem 0.75rem; margin: 0.5rem; font-size: 80%; }
.sm-html p.info::before, .sm-html p.success::before, .sm-html p.warning::before, .sm-html p.danger::before { display: inline-block; width: 1.5rem; height: 1.5rem; margin-right: 0.5rem; margin-top: 0.1rem; }
.sm-html p.info { border: 1px solid rgba(14,165,233,1); background-color: rgba(14,165,233,0.25); }
.sm-html p.info::before { color: rgba(14,165,233,1); content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z' fill='rgba(14,165,233,1)' /%3E%3C/svg%3E"); }
.sm-html p.success { border: 1px solid rgba(22,163,74,1); background-color: rgba(22,163,74,0.25); }
.sm-html p.success::before { color: rgba(22,163,74,1); content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z' fill='rgba(22,163,74,1)' /%3E%3C/svg%3E"); }
.sm-html p.warning { border: 1px solid rgba(202,138,4,1); background-color: rgba(250,204,21,0.25); }
.sm-html p.warning::before { color: rgba(202,138,4,1); content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M12,2L1,21H23M12,6L19.53,19H4.47M11,10V14H13V10M11,16V18H13V16' fill='rgba(202,138,4,1)' /%3E%3C/svg%3E"); }
.sm-html p.danger { border: 1px solid rgba(220,38,38,1); background-color: rgba(220,38,38,0.25); }
.sm-html p.danger::before { color: rgba(220,38,38,1); content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M8.27,3L3,8.27V15.73L8.27,21H15.73L21,15.73V8.27L15.73,3M8.41,7L12,10.59L15.59,7L17,8.41L13.41,12L17,15.59L15.59,17L12,13.41L8.41,17L7,15.59L10.59,12L7,8.41' fill='rgba(220,38,38,1)' /%3E%3C/svg%3E"); }
.sm-editor::-webkit-scrollbar { background-color: transparent; width: 16px; }
.sm-editor::-webkit-scrollbar-thumb { background-color: #aaa; border: 4px solid transparent; border-radius: 8px; background-clip: padding-box; }
@keyframes rotate{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
</style>
</head>