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