feat: Add simplemde for editing posts and pages
This commit is contained in:
parent
f7f5e5bf6b
commit
20ec2c3fd6
12 changed files with 209 additions and 13 deletions
15
apps/admin/lib/kaffy/editor_extension.ex
Normal file
15
apps/admin/lib/kaffy/editor_extension.ex
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
defmodule Admin.Kaffy.EditorExtension do
|
||||||
|
def stylesheets(_conn) do
|
||||||
|
[
|
||||||
|
{:safe, ~s(<link rel="stylesheet" href="/js/css/content-editor.css" />)},
|
||||||
|
{:safe, ~s(<link rel="stylesheet" href="/css/app.css" />)},
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def javascripts(_conn) do
|
||||||
|
[
|
||||||
|
{:safe, ~s(<script src="/js/content-editor.js"></script>)},
|
||||||
|
{:safe, ~s(<script src="/js/app.js"></script>)},
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
40
apps/content/lib/content/markup_field.ex
Normal file
40
apps/content/lib/content/markup_field.ex
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
defmodule Content.MarkupField do
|
||||||
|
use Ecto.Type
|
||||||
|
def type, do: :string
|
||||||
|
|
||||||
|
import Phoenix.HTML, only: [sigil_E: 2]
|
||||||
|
import Phoenix.HTML.Form, only: [textarea: 3, label: 2]
|
||||||
|
|
||||||
|
def cast(text) when is_binary(text) do
|
||||||
|
{:ok, text}
|
||||||
|
end
|
||||||
|
|
||||||
|
def cast(_), do: :error
|
||||||
|
|
||||||
|
def load(data) do
|
||||||
|
{:ok, data}
|
||||||
|
end
|
||||||
|
|
||||||
|
def dump(text) when is_binary(text), do: {:ok, text}
|
||||||
|
def dump(_), do: :error
|
||||||
|
|
||||||
|
def render_form(_conn, _changeset, form, field, admin_opts) do
|
||||||
|
rows = Map.get(admin_opts, :rows, 32)
|
||||||
|
|
||||||
|
~E"""
|
||||||
|
<div class="form-group ">
|
||||||
|
<%= label(form, field) %>
|
||||||
|
<%= textarea(form, field, [class: "form-control", rows: rows, "data-simplemde": true]) %>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_index(conn, resource, field, _opts) do
|
||||||
|
case Map.get(resource, field) do
|
||||||
|
nil ->
|
||||||
|
""
|
||||||
|
text ->
|
||||||
|
Content.PostsView.process_content(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,13 +4,13 @@ defmodule Content.Post do
|
||||||
"""
|
"""
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
alias Content.Slugs
|
alias Content.{MarkupField, Slugs}
|
||||||
|
|
||||||
@derive {Phoenix.Param, key: :name}
|
@derive {Phoenix.Param, key: :name}
|
||||||
schema "posts" do
|
schema "posts" do
|
||||||
field :date, :naive_datetime
|
field :date, :naive_datetime
|
||||||
field :date_gmt, :naive_datetime
|
field :date_gmt, :naive_datetime
|
||||||
field :content, :string, default: ""
|
field :content, MarkupField, default: ""
|
||||||
field :title, :string
|
field :title, :string
|
||||||
field :excerpt, :string
|
field :excerpt, :string
|
||||||
field :status, :string
|
field :status, :string
|
||||||
|
|
52
apps/content/test/content/markup_field_test.exs
Normal file
52
apps/content/test/content/markup_field_test.exs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
defmodule Content.MarkupFieldTest do
|
||||||
|
use Content.DataCase
|
||||||
|
|
||||||
|
import Content.MarkupField
|
||||||
|
import Phoenix.HTML, only: [safe_to_string: 1]
|
||||||
|
import Phoenix.HTML.Form, only: [form_for: 3]
|
||||||
|
|
||||||
|
def form do
|
||||||
|
:example
|
||||||
|
|> form_for(
|
||||||
|
"/example",
|
||||||
|
as: :test_params
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "underlying db type is string" do
|
||||||
|
assert type() == :string
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cast string to markup field" do
|
||||||
|
assert cast("foo") == {:ok, "foo"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cast nonstring to markup field is an error" do
|
||||||
|
assert cast(1) == :error
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dump string from markup field" do
|
||||||
|
assert dump("baz") == {:ok, "baz"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dump nonstring from markup field is error" do
|
||||||
|
assert dump(3) == :error
|
||||||
|
end
|
||||||
|
|
||||||
|
test "load value" do
|
||||||
|
assert load("bar") == {:ok, "bar"}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "render_form/5 makes a field with a simplemde data attribute" do
|
||||||
|
safe = render_form(nil, nil, form, :boop, %{})
|
||||||
|
|
||||||
|
assert safe_to_string(safe) =~ "data-simplemde"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "render_index/4 can render markdown" do
|
||||||
|
markup = render_index(nil, %{text: "# Test"}, :text, [])
|
||||||
|
|
||||||
|
assert markup =~ "<h1>"
|
||||||
|
assert markup =~ "Test"
|
||||||
|
end
|
||||||
|
end
|
11
apps/core/assets/css/content-editor-overrides.css
Normal file
11
apps/core/assets/css/content-editor-overrides.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.CodeMirror-fullscreen {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar.fullscreen {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-preview-active-side {
|
||||||
|
z-index: 1040;
|
||||||
|
}
|
|
@ -14,14 +14,7 @@ import "../css/app.scss"
|
||||||
// import socket from "./socket"
|
// import socket from "./socket"
|
||||||
//
|
//
|
||||||
import "phoenix_html"
|
import "phoenix_html"
|
||||||
|
import { ready } from "./utils"
|
||||||
function ready(fn) {
|
|
||||||
if (document.readyState != 'loading'){
|
|
||||||
fn();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function togglePasswordFieldVisibility()
|
function togglePasswordFieldVisibility()
|
||||||
{
|
{
|
||||||
|
|
34
apps/core/assets/js/content-editor.js
Normal file
34
apps/core/assets/js/content-editor.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { ready } from "./utils"
|
||||||
|
import SimpleMDE from "simplemde"
|
||||||
|
import "simplemde/dist/simplemde.min.css"
|
||||||
|
import "../css/content-editor-overrides.css"
|
||||||
|
|
||||||
|
|
||||||
|
const requestPreview = (plainText, previewContainer) => {
|
||||||
|
let request = new XMLHttpRequest()
|
||||||
|
const postForm = previewContainer.closest('form')
|
||||||
|
let formData = new FormData(postForm)
|
||||||
|
|
||||||
|
formData.set('post[content]', plainText)
|
||||||
|
|
||||||
|
request.addEventListener('load', function(event) {
|
||||||
|
previewContainer.innerHTML = event.target.responseText
|
||||||
|
})
|
||||||
|
|
||||||
|
request.open('POST', '/pages/posts/preview', true)
|
||||||
|
|
||||||
|
request.send(formData)
|
||||||
|
}
|
||||||
|
|
||||||
|
ready(() => {
|
||||||
|
document.querySelectorAll('[data-simplemde]').forEach(el => {
|
||||||
|
new SimpleMDE({
|
||||||
|
element: el,
|
||||||
|
previewRender: (plainText, previewContainer) => {
|
||||||
|
requestPreview(plainText, previewContainer)
|
||||||
|
|
||||||
|
return previewContainer.innerHTML
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
10
apps/core/assets/js/utils.js
Normal file
10
apps/core/assets/js/utils.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
const ready = (fn) => {
|
||||||
|
if (document.readyState != 'loading') {
|
||||||
|
fn()
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ready }
|
33
apps/core/assets/package-lock.json
generated
33
apps/core/assets/package-lock.json
generated
|
@ -3117,6 +3117,19 @@
|
||||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||||
},
|
},
|
||||||
|
"codemirror": {
|
||||||
|
"version": "5.56.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.56.0.tgz",
|
||||||
|
"integrity": "sha512-MfKVmYgifXjQpLSgpETuih7A7WTTIsxvKfSLGseTY5+qt0E1UD1wblZGM6WLenORo8sgmf+3X+WTe2WF7mufyw=="
|
||||||
|
},
|
||||||
|
"codemirror-spell-checker": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-HGYPkIlIPMtRE7m6nKGcP0mTNx4=",
|
||||||
|
"requires": {
|
||||||
|
"typo-js": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"collection-map": {
|
"collection-map": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz",
|
||||||
|
@ -8905,6 +8918,11 @@
|
||||||
"object-visit": "^1.0.0"
|
"object-visit": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"marked": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-mJzT8D2yPxoPh7h0UXkB+dBj4FykPJ2OIfxAWeIHrvoHDkFxukV/29QxoFQoPM6RLEwhIFdJpmKBlqVM3s2ZIw=="
|
||||||
|
},
|
||||||
"matchdep": {
|
"matchdep": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
|
||||||
|
@ -11762,6 +11780,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"simplemde": {
|
||||||
|
"version": "1.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/simplemde/-/simplemde-1.11.2.tgz",
|
||||||
|
"integrity": "sha1-ojo12XjSxA7wfewAjJLwcNjggOM=",
|
||||||
|
"requires": {
|
||||||
|
"codemirror": "*",
|
||||||
|
"codemirror-spell-checker": "*",
|
||||||
|
"marked": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"slash": {
|
"slash": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
|
||||||
|
@ -12839,6 +12867,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||||
},
|
},
|
||||||
|
"typo-js": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-W3kLbx+ML9PBl5Bzso/lTvVxk4BCveSNAtQeht59FEtxCdGThmn6wSHA4Xq3eQYAK24NHdisMM4JmsK0GFy/pg=="
|
||||||
|
},
|
||||||
"uglify-js": {
|
"uglify-js": {
|
||||||
"version": "2.8.29",
|
"version": "2.8.29",
|
||||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
"phoenix": "file:../deps/phoenix",
|
"phoenix": "file:../deps/phoenix",
|
||||||
"phoenix_html": "file:../deps/phoenix_html"
|
"phoenix_html": "file:../deps/phoenix_html",
|
||||||
|
"simplemde": "^1.11.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.0.0",
|
"@babel/core": "^7.0.0",
|
||||||
|
|
|
@ -18,7 +18,8 @@ module.exports = (env, options) => {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
entry: {
|
entry: {
|
||||||
'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
|
'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js']),
|
||||||
|
'content-editor': ['./js/content-editor.js'],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
|
@ -77,7 +78,10 @@ module.exports = (env, options) => {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new MiniCssExtractPlugin({ filename: '../css/app.css' }),
|
new MiniCssExtractPlugin({
|
||||||
|
filename: 'css/[name].css',
|
||||||
|
chunkFilename: '[id].css',
|
||||||
|
}),
|
||||||
new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
|
new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|
|
@ -3,6 +3,9 @@ use Mix.Config
|
||||||
config :kaffy,
|
config :kaffy,
|
||||||
otp_app: :admin,
|
otp_app: :admin,
|
||||||
ecto_repo: Admin.Repo,
|
ecto_repo: Admin.Repo,
|
||||||
|
extensions: [
|
||||||
|
Admin.Kaffy.EditorExtension,
|
||||||
|
],
|
||||||
router: Admin.Router,
|
router: Admin.Router,
|
||||||
resources: &Admin.Kaffy.Config.create_resources/1
|
resources: &Admin.Kaffy.Config.create_resources/1
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue