diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 19043af5f..c597bb5c9 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -454,7 +454,6 @@ (if (empty? rch-operations) rch (conj rch rchg)) (if (empty? uch-operations) uch (conj uch uchg))))))))))) - (defn update-shapes-recursive [ids f] (us/assert ::coll-of-uuid ids) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index aaa1d5a55..8f37b97ab 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -121,8 +121,8 @@ ;; --- TEXT EDITION IMPL (defn- update-shape - [shape pred-fn attrs] - (let [merge-attrs #(attrs/merge % attrs) + [shape pred-fn merge-fn attrs] + (let [merge-attrs #(merge-fn % attrs) transform #(txt/transform-nodes pred-fn merge-attrs %)] (update shape :content transform))) @@ -134,7 +134,7 @@ (let [objects (dwc/lookup-page-objects state) shape (get objects id) - update-fn #(update-shape % txt/is-root-node? attrs) + update-fn #(update-shape % txt/is-root-node? attrs/merge attrs) shape-ids (cond (= (:type shape) :text) [id] (= (:type shape) :group) (cp/get-children id objects))] @@ -154,7 +154,15 @@ (let [objects (dwc/lookup-page-objects state) shape (get objects id) - update-fn #(update-shape % txt/is-paragraph-node? attrs) + merge-fn (fn [node attrs] + (reduce-kv (fn [node k v] + (if (= (get node k) v) + (dissoc node k) + (assoc node k v))) + node + attrs)) + + update-fn #(update-shape % txt/is-paragraph-node? merge-fn attrs) shape-ids (cond (= (:type shape) :text) [id] (= (:type shape) :group) (cp/get-children id objects))] @@ -174,7 +182,7 @@ (let [objects (dwc/lookup-page-objects state) shape (get objects id) - update-fn #(update-shape % txt/is-text-node? attrs) + update-fn #(update-shape % txt/is-text-node? attrs/merge attrs) shape-ids (cond (= (:type shape) :text) [id] (= (:type shape) :group) (cp/get-children id objects))] (rx/of (dwc/update-shapes shape-ids update-fn)))))))) diff --git a/frontend/src/app/main/ui/shapes/text/styles.cljs b/frontend/src/app/main/ui/shapes/text/styles.cljs index 93f6cde2e..aaa01bf1f 100644 --- a/frontend/src/app/main/ui/shapes/text/styles.cljs +++ b/frontend/src/app/main/ui/shapes/text/styles.cljs @@ -47,7 +47,7 @@ (defn generate-paragraph-styles [shape data] (let [line-height (:line-height data) - text-align (:text-align data) + text-align (:text-align data "start") grow-type (:grow-type shape) base #js {:fontSize (str (:font-size txt/default-text-attrs) "px") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index c1c4c780e..4c8a78bf1 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -26,34 +26,62 @@ [cuerdas.core :as str] [rumext.alpha :as mf])) -(def text-typography-attrs [:typography-ref-id :typography-ref-file]) -(def text-fill-attrs [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient :fill :opacity ]) -(def text-font-attrs [:font-id :font-family :font-variant-id :font-size :font-weight :font-style]) -(def text-align-attrs [:text-align]) -(def text-spacing-attrs [:line-height :letter-spacing]) -(def text-valign-attrs [:vertical-align]) -(def text-decoration-attrs [:text-decoration]) -(def text-transform-attrs [:text-transform]) +(def text-typography-attrs + [:typography-ref-id + :typography-ref-file]) -(def shape-attrs [:grow-type]) -(def root-attrs (d/concat text-valign-attrs - text-align-attrs)) -(def paragraph-attrs text-align-attrs) -(def text-attrs (d/concat text-typography-attrs - text-font-attrs - text-align-attrs - text-spacing-attrs - text-decoration-attrs - text-transform-attrs)) +(def text-fill-attrs + [:fill-color + :fill-opacity + :fill-color-ref-id + :fill-color-ref-file + :fill-color-gradient]) + +(def text-font-attrs + [:font-id + :font-family + :font-variant-id + :font-size + :font-weight + :font-style]) + +(def text-align-attrs + [:text-align]) + +(def text-spacing-attrs + [:line-height + :letter-spacing]) + +(def text-valign-attrs + [:vertical-align]) + +(def text-decoration-attrs + [:text-decoration]) + +(def text-transform-attrs + [:text-transform]) + +(def shape-attrs + [:grow-type]) + +(def root-attrs + (d/concat text-valign-attrs text-align-attrs)) + +(def paragraph-attrs + text-align-attrs) + +(def text-attrs + (d/concat text-typography-attrs + text-font-attrs + text-spacing-attrs + text-decoration-attrs + text-transform-attrs)) (def attrs (d/concat #{} shape-attrs root-attrs paragraph-attrs text-attrs)) (mf/defc text-align-options [{:keys [ids values on-change] :as props}] (let [{:keys [text-align]} values - - text-align (or text-align "left") - handle-change (fn [event new-align] (on-change {:text-align new-align}))] @@ -169,13 +197,13 @@ {::mf/wrap [mf/memo]} [{:keys [ids type values] :as props}] - (let [current-file-id (mf/use-ctx ctx/current-file-id) + (let [file-id (mf/use-ctx ctx/current-file-id) typographies (mf/deref refs/workspace-file-typography) - shared-libs (mf/deref refs/workspace-libraries) - label (case type - :multiple (tr "workspace.options.text-options.title-selection") - :group (tr "workspace.options.text-options.title-group") - (tr "workspace.options.text-options.title")) + shared-libs (mf/deref refs/workspace-libraries) + label (case type + :multiple (tr "workspace.options.text-options.title-selection") + :group (tr "workspace.options.text-options.title-group") + (tr "workspace.options.text-options.title")) emit-update! (fn [id attrs] @@ -194,14 +222,14 @@ typography (cond (and (:typography-ref-id values) (not= (:typography-ref-id values) :multiple) - (not= (:typography-ref-file values) current-file-id)) + (not= (:typography-ref-file values) file-id)) (-> shared-libs (get-in [(:typography-ref-file values) :data :typographies (:typography-ref-id values)]) (assoc :file-id (:typography-ref-file values))) (and (:typography-ref-id values) (not= (:typography-ref-id values) :multiple) - (= (:typography-ref-file values) current-file-id)) + (= (:typography-ref-file values) file-id)) (get typographies (:typography-ref-id values))) on-convert-to-typography @@ -218,7 +246,7 @@ (let [id (uuid/next)] (st/emit! (dwl/add-typography (assoc typography :id id) false)) (run! #(emit-update! % {:typography-ref-id id - :typography-ref-file current-file-id}) ids))))) + :typography-ref-file file-id}) ids))))) handle-detach-typography (fn [] @@ -228,7 +256,7 @@ handle-change-typography (fn [changes] - (st/emit! (dwl/update-typography (merge typography changes) current-file-id))) + (st/emit! (dwl/update-typography (merge typography changes) file-id))) opts #js {:ids ids :values values @@ -244,7 +272,7 @@ (cond typography [:& typography-entry {:typography typography - :read-only? (not= (:typography-ref-file values) current-file-id) + :read-only? (not= (:typography-ref-file values) file-id) :file (get shared-libs (:typography-ref-file values)) :on-detach handle-detach-typography :on-change handle-change-typography}] diff --git a/frontend/src/app/util/draft_helpers.js b/frontend/src/app/util/draft_helpers.js deleted file mode 100644 index f75ee0277..000000000 --- a/frontend/src/app/util/draft_helpers.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) UXBOX Labs SL - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -'use strict'; - -import {CharacterMetadata} from "draft-js"; -import {Map} from "immutable"; - -function removeStylePrefix(chmeta, stylePrefix) { - var withoutStyle = chmeta.set('style', chmeta.getStyle().filter((s) => !s.startsWith(stylePrefix))) - return CharacterMetadata.create(withoutStyle); -}; - -export function removeInlineStylePrefix(contentState, selectionState, stylePrefix) { - var blockMap = contentState.getBlockMap(); - var startKey = selectionState.getStartKey(); - var startOffset = selectionState.getStartOffset(); - var endKey = selectionState.getEndKey(); - var endOffset = selectionState.getEndOffset(); - var newBlocks = blockMap.skipUntil(function (_, k) { - return k === startKey; - }).takeUntil(function (_, k) { - return k === endKey; - }).concat(Map([[endKey, blockMap.get(endKey)]])).map(function (block, blockKey) { - var sliceStart; - var sliceEnd; - - if (startKey === endKey) { - sliceStart = startOffset; - sliceEnd = endOffset; - } else { - sliceStart = blockKey === startKey ? startOffset : 0; - sliceEnd = blockKey === endKey ? endOffset : block.getLength(); - } - - var chars = block.getCharacterList(); - var current; - - while (sliceStart < sliceEnd) { - current = chars.get(sliceStart); - chars = chars.set(sliceStart, removeStylePrefix(current, stylePrefix)); - sliceStart++; - } - - return block.set('characterList', chars); - }); - - return contentState.merge({ - blockMap: blockMap.merge(newBlocks), - selectionBefore: selectionState, - selectionAfter: selectionState - }); -} diff --git a/frontend/src/app/util/text_editor.cljs b/frontend/src/app/util/text_editor.cljs index 26a3fa8b2..3d90a73c1 100644 --- a/frontend/src/app/util/text_editor.cljs +++ b/frontend/src/app/util/text_editor.cljs @@ -11,7 +11,7 @@ "Draft related abstraction functions." (:require ["draft-js" :as draft] - ["./draft_helpers.js" :as helpers] + ["./text_editor_impl.js" :as impl] [app.common.attrs :as attrs] [app.common.text :as txt] [app.common.data :as d] @@ -206,27 +206,15 @@ (defn create-editor-state ([] - (.createEmpty ^js draft/EditorState)) + (impl/createEditorState nil nil)) ([content] - (.createWithContent ^js draft/EditorState content)) + (impl/createEditorState content nil)) ([content decorator] - (if (some? content) - (.createWithContent ^js draft/EditorState content decorator) - (.createEmpty ^js draft/EditorState decorator)))) + (impl/createEditorState content decorator))) (defn create-decorator [type component] - (letfn [(find-entity [block callback content] - (.findEntityRanges ^js block - (fn [cmeta] - (let [ekey (.getEntity ^js cmeta)] - (boolean - (and (some? ekey) - (= type (.. ^js content (getEntity ekey) (getType))))))) - callback))] - (draft/CompositeDecorator. - #js [#js {:strategy find-entity - :component component}]))) + (impl/createDecorator type component)) (defn import-content [content] @@ -248,18 +236,7 @@ (defn editor-select-all [state] - (let [content (get-editor-current-content state) - fblock (.. ^js content getBlockMap first) - lblock (.. ^js content getBlockMap last) - fbk (.getKey ^js fblock) - lbk (.getKey ^js lblock) - lbl (.getLength ^js lblock) - params #js {:anchorKey fbk - :anchorOffset 0 - :focusKey lbk - :focusOffset lbl} - selection (draft/SelectionState. params)] - (.forceSelection ^js draft/EditorState state selection))) + (impl/selectAll state)) (defn get-editor-block-data [block] @@ -272,9 +249,7 @@ (defn get-editor-current-block-data [state] - (let [content (.getCurrentContent ^js state) - key (.. ^js state getSelection getStartKey) - block (.getBlockForKey ^js content key)] + (let [block (impl/getCurrentBlock state)] (get-editor-block-data block))) (defn get-editor-current-inline-styles @@ -284,103 +259,20 @@ (defn update-editor-current-block-data [state attrs] - (loop [selection (.getSelection ^js state) - start-key (.getStartKey ^js selection) - end-key (.getEndKey ^js selection) - content (.getCurrentContent ^js state) - target selection] - (if (and (not= start-key end-key) - (zero? (.getEndOffset ^js selection))) - (let [before-block (.getBlockBefore ^js content end-key)] - (recur selection - start-key - (.getKey ^js before-block) - content - (.merge ^js target - #js {:anchorKey start-key - :anchorOffset (.getStartOffset ^js selection) - :focusKey end-key - :focusOffset (.getLength ^js before-block) - :isBackward false}))) - (.push ^js draft/EditorState - state - (.mergeBlockData ^js draft/Modifier content target (clj->js attrs)) - "change-block-data")))) - -(defn get-editor-current-entity-key - [state] - (let [content (.getCurrentContent ^js state) - selection (.getSelection ^js state) - start-key (.getStartKey ^js selection) - start-offset (.getStartOffset ^js selection) - block (.getBlockForKey ^js content start-key)] - (.getEntityAt ^js block start-offset))) + (impl/updateCurrentBlockData state (clj->js attrs))) (defn update-editor-current-inline-styles [state attrs] - (let [selection (.getSelection ^js state) - styles (attrs-to-styles attrs)] - (reduce (fn [state style] - (let [[sk sv] (decode-style style) - prefix (encode-style-prefix sk) - - content (.getCurrentContent ^js state) - content (helpers/removeInlineStylePrefix content - selection - prefix) - - content (.applyInlineStyle ^js draft/Modifier - content - selection - style)] - (.push ^js draft/EditorState state content "change-inline-style"))) - state - styles))) + (impl/applyInlineStyle state (attrs-to-styles attrs))) (defn editor-split-block [state] - (let [content (.getCurrentContent ^js state) - selection (.getSelection ^js state) - content (.splitBlock ^js draft/Modifier content selection) - block-data (.. ^js content -blockMap (get (.. content -selectionBefore getStartKey)) getData) - block-key (.. ^js content -selectionAfter getStartKey) - block-map (.. ^js content -blockMap (update block-key (fn [block] (.set ^js block "data" block-data))))] - (.push ^js draft/EditorState state (.set ^js content "blockMap" block-map) "split-block"))) + (impl/splitBlockPreservingData state)) (defn add-editor-blur-selection [state] - (let [content (.getCurrentContent ^js state) - selection (.getSelection ^js state) - content (.createEntity ^js content "PENPOT_SELECTION" "MUTABLE") - ekey (.getLastCreatedEntityKey ^js content) - content (.applyEntity draft/Modifier - content - selection - ekey)] - (.push draft/EditorState state content "apply-entity"))) - + (impl/addBlurSelectionEntity state)) (defn remove-editor-blur-selection [state] - (let [content (get-editor-current-content state) - fblock (.. ^js content getBlockMap first) - lblock (.. ^js content getBlockMap last) - fbk (.getKey ^js fblock) - lbk (.getKey ^js lblock) - lbl (.getLength ^js lblock) - params #js {:anchorKey fbk - :anchorOffset 0 - :focusKey lbk - :focusOffset lbl} - - prev-selection (.getSelection state) - - selection (draft/SelectionState. params) - content (.applyEntity draft/Modifier - content - selection - nil)] - (as-> state $ - (.push draft/EditorState $ content "apply-entity") - (.forceSelection ^js draft/EditorState $ prev-selection)))) - + (impl/removeBlurSelectionEntity state)) diff --git a/frontend/src/app/util/text_editor_impl.js b/frontend/src/app/util/text_editor_impl.js new file mode 100644 index 000000000..e41422656 --- /dev/null +++ b/frontend/src/app/util/text_editor_impl.js @@ -0,0 +1,212 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. + * + * Copyright (c) UXBOX Labs SL + */ + +'use strict'; + +import { + CharacterMetadata, + EditorState, + CompositeDecorator, + SelectionState, + Modifier +} from "draft-js"; + +import {Map} from "immutable"; + +function isDefined(v) { + return v !== undefined && v !== null; +} + +export function createEditorState(content, decorator) { + if (content === null) { + return EditorState.createEmpty(decorator); + } else { + return EditorState.createWithContent(content, decorator); + } +} + +export function createDecorator(type, component) { + const strategy = (block, callback, content) => { + return block.findEntityRanges((cmeta) => { + const entityKey = cmeta.getEntity(); + return isDefined(entityKey) && (type === content.getEntity(entityKey).getType()); + }, callback); + }; + + return new CompositeDecorator([ + {strategy, component} + ]); +} + +function getSelectAllSelection(state) { + const content = state.getCurrentContent(); + const firstBlock = content.getBlockMap().first(); + const lastBlock = content.getBlockMap().last(); + + return new SelectionState({ + "anchorKey": firstBlock.getKey(), + "anchorOffset": 0, + "focusKey": lastBlock.getKey(), + "focusOffset": lastBlock.getLength() + }); +} + +export function selectAll(state) { + return EditorState.forceSelection(state, getSelectAllSelection(state)); +} + +function modifySelectedBlocks(contentState, selectionState, operation) { + var startKey = selectionState.getStartKey(); + var endKey = selectionState.getEndKey(); + var blockMap = contentState.getBlockMap(); + + var newBlocks = blockMap.toSeq().skipUntil(function (_, k) { + return k === startKey; + }).takeUntil(function (_, k) { + return k === endKey; + }).concat(Map([[endKey, blockMap.get(endKey)]])).map(operation); + + return contentState.merge({ + blockMap: blockMap.merge(newBlocks), + selectionBefore: selectionState, + selectionAfter: selectionState + }); +} + +export function updateCurrentBlockData(state, attrs) { + const selection = state.getSelection(); + let content = state.getCurrentContent(); + + content = modifySelectedBlocks(content, selection, (block) => { + let data = block.getData(); + for (let key of Object.keys(attrs)) { + const oldVal = data.get(key); + if (oldVal === attrs[key]) { + data = data.delete(key); + } else { + data = data.set(key, attrs[key]); + } + } + + return block.merge({ + data: data + }); + }); + + return EditorState.push(state, content, "change-block-data"); +} + +export function applyInlineStyle(state, styles) { + const selection = state.getSelection(); + + let state = state; + let content = null; + + for (let style of styles) { + const [p, k, _] = style.split("$$$"); + const prefix = [p, k, ""].join("$$$"); + + content = state.getCurrentContent(); + content = removeInlineStylePrefix(content, selection, prefix); + content = Modifier.applyInlineStyle(content, selection, style); + state = EditorState.push(state, content, "change-inline-style"); + } + + return state; +} + +export function splitBlockPreservingData(state) { + let content = state.getCurrentContent(); + const selection = state.getSelection(); + + content = Modifier.splitBlock(content, selection); + + const blockData = content.blockMap.get(content.selectionBefore.getStartKey()).getData(); + const blockKey = content.selectionAfter.getStartKey(); + const blockMap = content.blockMap.update(blockKey, (block) => { + return block.set("data", blockData); + }); + + content = content.set("blockMap", blockMap); + + return EditorState.push(state, content, "split-block"); +} + +export function addBlurSelectionEntity(state) { + let content = state.getCurrentContent(state); + const selection = state.getSelection(); + + content = content.createEntity("PENPOT_SELECTION", "MUTABLE"); + const entityKey = content.getLastCreatedEntityKey(); + + content = Modifier.applyEntity(content, selection, entityKey); + return EditorState.push(state, content, "apply-entity"); +} + +export function removeBlurSelectionEntity(state) { + const selectionAll = getSelectAllSelection(state); + const selection = state.getSelection(); + + let content = state.getCurrentContent(); + content = Modifier.applyEntity(content, selectionAll, null); + + state = EditorState.push(state, content, "apply-entity"); + state = EditorState.forceSelection(state, selection); + + return state; +} + +export function getCurrentBlock(state) { + const content = state.getCurrentContent(); + const selection = state.getSelection(); + const startKey = selection.getStartKey(); + return content.getBlockForKey(startKey); +} + +export function getCurrentEntityKey(state) { + const block = getCurrentBlock(state); + const selection = state.getSelection(); + const startOffset = selection.getStartOffset(); + return block.getEntityAt(startOffset); +} + +export function removeInlineStylePrefix(contentState, selectionState, stylePrefix) { + const startKey = selectionState.getStartKey(); + const startOffset = selectionState.getStartOffset(); + const endKey = selectionState.getEndKey(); + const endOffset = selectionState.getEndOffset(); + + return modifySelectedBlocks(contentState, selectionState, (block, blockKey) => { + let sliceStart; + let sliceEnd; + + if (startKey === endKey) { + sliceStart = startOffset; + sliceEnd = endOffset; + } else { + sliceStart = blockKey === startKey ? startOffset : 0; + sliceEnd = blockKey === endKey ? endOffset : block.getLength(); + } + + let chars = block.getCharacterList(); + let current; + + while (sliceStart < sliceEnd) { + current = chars.get(sliceStart); + current = current.set("style", current.getStyle().filter((s) => !s.startsWith(stylePrefix))) + chars = chars.set(sliceStart, CharacterMetadata.create(current)); + + sliceStart++; + } + + return block.set('characterList', chars); + }); +}