diff --git a/apps/admin/lib/kaffy/editor_extension.ex b/apps/admin/lib/kaffy/editor_extension.ex
new file mode 100644
index 00000000..620dfd8e
--- /dev/null
+++ b/apps/admin/lib/kaffy/editor_extension.ex
@@ -0,0 +1,15 @@
+defmodule Admin.Kaffy.EditorExtension do
+ def stylesheets(_conn) do
+ [
+ {:safe, ~s( )},
+ {:safe, ~s( )},
+ ]
+ end
+
+ def javascripts(_conn) do
+ [
+ {:safe, ~s()},
+ {:safe, ~s()},
+ ]
+ end
+end
diff --git a/apps/content/lib/content/markup_field.ex b/apps/content/lib/content/markup_field.ex
new file mode 100644
index 00000000..fde448d6
--- /dev/null
+++ b/apps/content/lib/content/markup_field.ex
@@ -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"""
+
+ <%= label(form, field) %>
+ <%= textarea(form, field, [class: "form-control", rows: rows, "data-simplemde": true]) %>
+
+ """
+ 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
diff --git a/apps/content/lib/content/post.ex b/apps/content/lib/content/post.ex
index e429c8a5..83deb7c9 100644
--- a/apps/content/lib/content/post.ex
+++ b/apps/content/lib/content/post.ex
@@ -4,13 +4,13 @@ defmodule Content.Post do
"""
use Ecto.Schema
import Ecto.Changeset
- alias Content.Slugs
+ alias Content.{MarkupField, Slugs}
@derive {Phoenix.Param, key: :name}
schema "posts" do
field :date, :naive_datetime
field :date_gmt, :naive_datetime
- field :content, :string, default: ""
+ field :content, MarkupField, default: ""
field :title, :string
field :excerpt, :string
field :status, :string
diff --git a/apps/content/test/content/markup_field_test.exs b/apps/content/test/content/markup_field_test.exs
new file mode 100644
index 00000000..c54e6260
--- /dev/null
+++ b/apps/content/test/content/markup_field_test.exs
@@ -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 =~ ""
+ assert markup =~ "Test"
+ end
+end
diff --git a/apps/core/assets/css/content-editor-overrides.css b/apps/core/assets/css/content-editor-overrides.css
new file mode 100644
index 00000000..d9f233c5
--- /dev/null
+++ b/apps/core/assets/css/content-editor-overrides.css
@@ -0,0 +1,11 @@
+.CodeMirror-fullscreen {
+ z-index: 1040;
+}
+
+.editor-toolbar.fullscreen {
+ z-index: 1040;
+}
+
+.editor-preview-active-side {
+ z-index: 1040;
+}
diff --git a/apps/core/assets/js/app.js b/apps/core/assets/js/app.js
index d9be5a7d..75589b07 100644
--- a/apps/core/assets/js/app.js
+++ b/apps/core/assets/js/app.js
@@ -14,14 +14,7 @@ import "../css/app.scss"
// import socket from "./socket"
//
import "phoenix_html"
-
-function ready(fn) {
- if (document.readyState != 'loading'){
- fn();
- } else {
- document.addEventListener('DOMContentLoaded', fn);
- }
-}
+import { ready } from "./utils"
function togglePasswordFieldVisibility()
{
diff --git a/apps/core/assets/js/content-editor.js b/apps/core/assets/js/content-editor.js
new file mode 100644
index 00000000..c467b4f6
--- /dev/null
+++ b/apps/core/assets/js/content-editor.js
@@ -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
+ },
+ })
+ })
+})
diff --git a/apps/core/assets/js/utils.js b/apps/core/assets/js/utils.js
new file mode 100644
index 00000000..3d60005e
--- /dev/null
+++ b/apps/core/assets/js/utils.js
@@ -0,0 +1,10 @@
+
+const ready = (fn) => {
+ if (document.readyState != 'loading') {
+ fn()
+ } else {
+ document.addEventListener('DOMContentLoaded', fn)
+ }
+}
+
+export { ready }
diff --git a/apps/core/assets/package-lock.json b/apps/core/assets/package-lock.json
index 15f21191..4af03ae9 100644
--- a/apps/core/assets/package-lock.json
+++ b/apps/core/assets/package-lock.json
@@ -3117,6 +3117,19 @@
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz",
@@ -8905,6 +8918,11 @@
"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": {
"version": "2.0.0",
"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": {
"version": "1.0.0",
"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",
"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": {
"version": "2.8.29",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
diff --git a/apps/core/assets/package.json b/apps/core/assets/package.json
index d75d7538..99d4c0a9 100644
--- a/apps/core/assets/package.json
+++ b/apps/core/assets/package.json
@@ -11,7 +11,8 @@
"dependencies": {
"gulp": "^4.0.2",
"phoenix": "file:../deps/phoenix",
- "phoenix_html": "file:../deps/phoenix_html"
+ "phoenix_html": "file:../deps/phoenix_html",
+ "simplemde": "^1.11.2"
},
"devDependencies": {
"@babel/core": "^7.0.0",
diff --git a/apps/core/assets/webpack.config.js b/apps/core/assets/webpack.config.js
index 34ec3f42..c8507189 100644
--- a/apps/core/assets/webpack.config.js
+++ b/apps/core/assets/webpack.config.js
@@ -18,7 +18,8 @@ module.exports = (env, options) => {
]
},
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: {
filename: '[name].js',
@@ -77,7 +78,10 @@ module.exports = (env, options) => {
]
},
plugins: [
- new MiniCssExtractPlugin({ filename: '../css/app.css' }),
+ new MiniCssExtractPlugin({
+ filename: 'css/[name].css',
+ chunkFilename: '[id].css',
+ }),
new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
],
resolve: {
diff --git a/config/admin.exs b/config/admin.exs
index 03340e4d..e5c48e15 100644
--- a/config/admin.exs
+++ b/config/admin.exs
@@ -3,6 +3,9 @@ use Mix.Config
config :kaffy,
otp_app: :admin,
ecto_repo: Admin.Repo,
+ extensions: [
+ Admin.Kaffy.EditorExtension,
+ ],
router: Admin.Router,
resources: &Admin.Kaffy.Config.create_resources/1