diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index e1443ec2..890c1420 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -242,18 +242,37 @@ svg { } } -.sutty-editor-prosemirror { +.sutty-editor-full { + $border-color: rgba(0, 0, 0, .2); border-radius: 4px; - border: 1px solid rgba(0, 0, 0, 0.2); - padding: 5px 0; - max-height: 450px; - overflow-y: auto; + border: 1px solid $border-color; + background: var(--background); - .ProseMirror { - padding: 4px 10px; - line-height: 1.2; - outline: none; - word-wrap: break-word; - white-space: pre-wrap; + blockquote { + padding: .75rem 1rem; + border-left: 4px solid var(--color); + p { margin: 0; } + } + + .sutty-editor-toolbar { + position: sticky; + top: 0; + padding: .5rem; + border-bottom: 1px solid $border-color; + border-radius: 4px 4px 0 0; + background: var(--background); + } + + .sutty-editor-prosemirror { + padding: 5px 0; + max-height: 450px; + overflow-y: auto; + .ProseMirror { + padding: 4px 10px; + line-height: 1.2; + outline: none; + word-wrap: break-word; + white-space: pre-wrap; + } } } diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 10dd772a..9f78638c 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -18,6 +18,7 @@ import {EditorState} from "prosemirror-state" import {EditorView} from "prosemirror-view" import {Schema, DOMParser} from "prosemirror-model" +import {keymap} from "prosemirror-keymap" // seleccionar cosas que no son texto import {gapCursor} from "prosemirror-gapcursor" @@ -31,12 +32,76 @@ import {inputRules, wrappingInputRule, textblockTypeInputRule, import {schema, defaultMarkdownParser, defaultMarkdownSerializer} from "prosemirror-markdown" +import {toggleMark, setBlockType, wrapIn, baseKeymap} from "prosemirror-commands" +import {toggleWrap, toggleBlockType, toggleList, + updateMark, removeMark} from "tiptap-commands" + import { DirectUpload } from "@rails/activestorage" +const toggleBold = toggleMark(schema.marks.strong) +const toggleItalic = toggleMark(schema.marks.em) +const wrapInQuote = toggleWrap(schema.nodes.blockquote) + +const toggleUl = toggleList(schema.nodes.bullet_list, schema.nodes.list_item) +const toggleOl = toggleList(schema.nodes.ordered_list, schema.nodes.list_item) + +const generateHeadingToggle = level => + toggleBlockType( + schema.nodes.heading, + schema.nodes.paragraph, + { level: level }, + ) +const toggleH1 = generateHeadingToggle(1) +const toggleH2 = generateHeadingToggle(2) + +const makeLink = (state, dispatch) => { + const {tr, selection, doc} = state + const type = schema.marks.link + + const {from, to} = selection + const has = doc.rangeHasMark(from, to, type) + + if (has) { + tr.removeMark(from, to, type) + } else { + console.log(tr.addMark(from, to, type.create({ + href: window.prompt('elegir url', '//sutty.nl') + }))) + } + + return dispatch(tr) +} + +console.log(schema) + class SuttyEditor { - constructor ({element, markdown}) { + constructor ({element, markdown, toolbar}) { this.state = this.buildState(markdown) this.view = this.buildView(element, this.state) + this.hooks(toolbar) + } + + runCommand (command) { + return command(this.view.state, this.view.dispatch, this.view) + } + + hooks (el) { + const setupBtn = (class_, action) => { + const btnEl = el.querySelector('.' + class_ + '-btn') + btnEl.type = 'button' + btnEl.addEventListener('click', e => { + action() + return false + }) + } + setupBtn('bold', () => this.runCommand(toggleBold)) + setupBtn('italic', () => this.runCommand(toggleItalic)) + setupBtn('quote', () => this.runCommand(wrapInQuote)) + setupBtn('link', () => this.runCommand(makeLink)) + setupBtn('ul', () => this.runCommand(toggleUl)) + setupBtn('ol', () => this.runCommand(toggleOl)) + setupBtn('h1', () => this.runCommand(toggleH1)) + setupBtn('h2', () => this.runCommand(toggleH2)) } buildState (markdown) { @@ -47,6 +112,7 @@ class SuttyEditor { } : {}), plugins: [ gapCursor(), + keymap(baseKeymap), inputRules({ rules: [ //emDash, // -- => — @@ -75,6 +141,11 @@ document.addEventListener('turbolinks:load', e => { `.sutty-editor-content[data-sutty-identifier="${el.dataset.suttyIdentifier}"]` ) + // conseguir toolbar + const toolbarEl = document.querySelector( + `.sutty-editor-toolbar[data-sutty-identifier="${el.dataset.suttyIdentifier}"]` + ) + // ocultarlo (no se oculta originalmente para usuaries noscript textareaEl.style.display = 'none' @@ -82,6 +153,7 @@ document.addEventListener('turbolinks:load', e => { const editor = new SuttyEditor({ element: el, markdown: textareaEl.value, + toolbar: toolbarEl, }) // hookear a la form para inyectar el contenido del editor al textarea oculto para que se mande diff --git a/app/views/application/_editor.haml b/app/views/application/_editor.haml index 94838bec..f20aa204 100644 --- a/app/views/application/_editor.haml +++ b/app/views/application/_editor.haml @@ -1,4 +1,15 @@ -.sutty-editor.sutty-editor-prosemirror{ 'data-sutty-identifier': identifier } +.sutty-editor-full + .sutty-editor-toolbar{ 'data-sutty-identifier': identifier } + %button.bold-btn= "bold" + %button.italic-btn= "italic" + %button.quote-btn= "quote" + %button.ul-btn= "ul" + %button.ol-btn= "ol" + %button.link-btn= "link" + %button.img-btn= "image" + %button.h1-btn= "h1" + %button.h2-btn= "h2" + .sutty-editor.sutty-editor-prosemirror.content{ 'data-sutty-identifier': identifier } %textarea.form-control.sutty-editor.sutty-editor-content{ rows: 8, 'data-sutty-identifier': identifier, name: name } = markdown diff --git a/package.json b/package.json index 76bf0ca1..1f9e08ad 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,16 @@ "@rails/webpacker": "^4.0.7", "commonmark": "^0.29.0", "input-tag": "https://0xacab.org/sutty/input-tag.git", - "table-dragger": "https://0xacab.org/sutty/table-dragger.git", + "prosemirror-commands": "^1.0.8", "prosemirror-gapcursor": "^1.0.4", "prosemirror-inputrules": "^1.0.4", + "prosemirror-keymap": "^1.0.2", "prosemirror-markdown": "^1.3.1", "prosemirror-model": "^1.7.4", "prosemirror-state": "^1.2.4", "prosemirror-view": "^1.11.7", + "table-dragger": "https://0xacab.org/sutty/table-dragger.git", + "tiptap-commands": "^1.12.3", "zepto": "^1.2.0" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index bc8f0db9..6aea8946 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5597,6 +5597,15 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= +prosemirror-commands@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.0.8.tgz#3b21e8b2f2e1c04ddb831b2d4055084b6d627bcc" + integrity sha512-P9QdkYYBHWsrJ1JztQuHgeZS7DPCcijQduOj9oxFiqK8Fm6eTsVHzU1IwiRBe+FlK7tyQaerhu/F5K8sqnZ1Cw== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + prosemirror-gapcursor@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.4.tgz#4ba663fb8511616e18ad222c904403cfbf6866dc" @@ -5615,7 +5624,7 @@ prosemirror-inputrules@^1.0.4: prosemirror-state "^1.0.0" prosemirror-transform "^1.0.0" -prosemirror-keymap@^1.0.0: +prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.2.tgz#e4e8b876a586ec6a170615fce19c4450f4075bef" integrity sha512-aq3fBT3WMbwGNacUtMbS/mxd87hjJyjtUx5/h3q/P3FiVqHxmeA9snxQsZHYe0cWRziZePun8zw6kHFKLp/DAQ== @@ -5638,6 +5647,14 @@ prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.7.4: dependencies: orderedmap "^1.0.0" +prosemirror-schema-list@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.0.4.tgz#cbb11936e306aa45586af4279529ce61b52cfb6e" + integrity sha512-7Y0b6FIG6ATnCcDSLrZfU9yIfOG5Yad3DMNZ9W7GGfMSzdIl0aHExrsIUgviJZjovO2jtLJVbxWGjMR3OrTupA== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-transform "^1.0.0" + prosemirror-state@^1.0.0, prosemirror-state@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.2.4.tgz#aab932ebb33a0f27c256abce6fc20da8ca77ad5e" @@ -5646,6 +5663,17 @@ prosemirror-state@^1.0.0, prosemirror-state@^1.2.4: prosemirror-model "^1.0.0" prosemirror-transform "^1.0.0" +prosemirror-tables@^0.9.5: + version "0.9.5" + resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.9.5.tgz#94d9881a46051e6fff3c51edffafa346da084def" + integrity sha512-RlAF/D7OvnDCOL8B6Qt6KuBkb0w3SedTdrou7wH7Nn2ml7+M5xUalW/h1f7dMD3wjsU47/Cn8zTbEkCDIpIggw== + dependencies: + prosemirror-keymap "^1.0.0" + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + prosemirror-view "^1.0.0" + prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0: version "1.1.5" resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.1.5.tgz#e50113d8004854eca1fa0711806bca53b1f1fad9" @@ -5653,6 +5681,11 @@ prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0: dependencies: prosemirror-model "^1.0.0" +prosemirror-utils@^0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.9.6.tgz#3d97bd85897e3b535555867dc95a51399116a973" + integrity sha512-UC+j9hQQ1POYfMc5p7UFxBTptRiGPR7Kkmbl3jVvU8VgQbkI89tR/GK+3QYC8n+VvBZrtAoCrJItNhWSxX3slA== + prosemirror-view@^1.0.0, prosemirror-view@^1.11.7: version "1.11.7" resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.11.7.tgz#537020acab43e18d51e18717fb1f39466317ce21" @@ -6795,6 +6828,30 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= +tiptap-commands@^1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.12.3.tgz#604767878073e6344d1daf7a376fd89fc62e4742" + integrity sha512-Dck51lePBwuHmkvkJ6+8V3DbInxAhZwtS2mPvVwz74pDUIcy17tCFw1eHUN50JoXIAci7acuxPKO/weVO1JAyw== + dependencies: + prosemirror-commands "^1.0.8" + prosemirror-inputrules "^1.0.4" + prosemirror-model "^1.7.4" + prosemirror-schema-list "^1.0.4" + prosemirror-state "^1.2.4" + prosemirror-tables "^0.9.5" + prosemirror-utils "^0.9.6" + tiptap-utils "^1.8.2" + +tiptap-utils@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.8.2.tgz#f07a2053c6ac9fbbb4f02e0844b326d0e6c8b7fb" + integrity sha512-pyx+3p4fICGM7JU1mcsnRx5jXvLrCL8Nm/9yjeWEZXpAC85L/btY0eFo2Oz4+dKg39+1EGNHheodujx3ngw4lQ== + dependencies: + prosemirror-model "^1.7.4" + prosemirror-state "^1.2.4" + prosemirror-tables "^0.9.5" + prosemirror-utils "^0.9.6" + to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"