Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Tinymce module into a web component #2555

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions app/assets/javascripts/alchemy/alchemy.dialog.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ class window.Alchemy.Dialog
@overlay.removeClass('open') if @overlay?
@$document.on 'webkitTransitionEnd transitionend oTransitionEnd', =>
@$document.off 'webkitTransitionEnd transitionend oTransitionEnd'
Alchemy.Tinymce.removeFrom $('.tinymce', @dialog_body)
@dialog_container.remove()
@overlay.remove() if @overlay?
@$body.removeClass('prevent-scrolling')
Expand Down Expand Up @@ -105,9 +104,6 @@ class window.Alchemy.Dialog
# Initializes the Dialog body
init: ->
Alchemy.GUI.init(@dialog_body)
Alchemy.Tinymce.initWith
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way with custom elements to get back this behavior? Admittedly it's a rarely used feature, but we would at least tell people how to upgrade.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible, but only on the component itself:

Aufnahme 2023-08-28 at 22 51 39@2x Aufnahme 2023-08-28 at 22 51 46@2x

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This screenshot is now outdated. Can you comment on the PR description how to upgrade this?

selector: ".alchemy-dialog-body textarea.tinymce",
width: '65%'
$('#overlay_tabs', @dialog_body).tabs()
@watch_remote_forms()

Expand Down
12 changes: 0 additions & 12 deletions app/assets/javascripts/alchemy/alchemy.dragndrop.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ $.extend Alchemy,
Alchemy.initializedSortableElements = false
$sortable_area = $(selector)

getTinymceIDs = (ui) ->
ids = []
$textareas = ui.item.find('textarea.has_tinymce')
$($textareas).each ->
id = this.id.replace(/tinymce_/, '')
ids.push parseInt(id, 10)
return ids

sortable_options =
items: "> .element-editor"
handle: "> .element-header .element-handle"
Expand Down Expand Up @@ -55,23 +47,19 @@ $.extend Alchemy,
$this = $(this)
name = ui.item.data('element-name')
$dropzone = $("[data-droppable-elements~='#{name}']")
ids = getTinymceIDs(ui)
$this.sortable('option', 'connectWith', $dropzone)
$this.sortable('refresh')
$dropzone.css('minHeight', 36)
ui.item.addClass('dragged')
if ui.item.hasClass('compact')
ui.placeholder.addClass('compact').css
height: ui.item.outerHeight()
Alchemy.Tinymce.remove(ids)
sascha-karnatz marked this conversation as resolved.
Show resolved Hide resolved
return
stop: (event, ui) ->
ids = getTinymceIDs(ui)
name = ui.item.data('element-name')
$dropzone = $("[data-droppable-elements~='#{name}']")
$dropzone.css('minHeight', '')
ui.item.removeClass('dragged')
Alchemy.Tinymce.init(ids)
return

$sortable_area.sortable(sortable_options)
Expand Down
2 changes: 1 addition & 1 deletion app/assets/stylesheets/alchemy/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ form {
> textarea,
> .select2-container,
> .autocomplete_tag_list,
> .mce-tinymce,
> .tinymce_container,
> .with-hint {
width: $form-right-width;
float: right;
Expand Down
8 changes: 1 addition & 7 deletions app/javascript/alchemy_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import ImageCropper from "alchemy_admin/image_cropper"
import Initializer from "alchemy_admin/initializer"
import Sitemap from "alchemy_admin/sitemap"
import Spinner from "alchemy_admin/spinner"
import Tinymce from "alchemy_admin/tinymce"
import PagePublicationFields from "alchemy_admin/page_publication_fields"

// Web Components
import "alchemy_admin/components/char_counter"
import "alchemy_admin/components/tinymce"
import "alchemy_admin/components/tooltip"
import "alchemy_admin/components/datepicker"
import "alchemy_admin/components/spinner"
Expand All @@ -42,13 +42,7 @@ Object.assign(Alchemy, {
IngredientAnchorLink,
Sitemap,
Spinner,
Tinymce,
PagePublicationFields
})

$(document).on("turbo:load", Initializer)

$(document).on("turbo:before-fetch-request", function () {
Alchemy.Tinymce.removeIntersectionObserver()
Alchemy.Tinymce.removeFrom($(".has_tinymce"))
})
107 changes: 107 additions & 0 deletions app/javascript/alchemy_admin/components/tinymce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { createHtmlElement, wrap } from "alchemy_admin/utils/dom_helpers"
import Spinner from "alchemy_admin/spinner"

class Tinymce extends HTMLTextAreaElement {
constructor() {
super()

// create a wrapper around the the textarea and place everything inside that container
this.container = createHtmlElement('<div class="tinymce_container" />')
wrap(this, this.container)
this.className = "has_tinymce"
}

/**
* the observer will initialize Tinymce if the textarea becomes visible
*/
connectedCallback() {
const observerCallback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0) {
this.initTinymceEditor()
// disable observer after the Tinymce was initialized
observer.unobserve(entry.target)
}
})
}

const options = {
root: document.getElementById("element_area"),
rootMargin: "0px",
threshold: [0.05]
}

this.tinymceIntersectionObserver = new IntersectionObserver(
observerCallback,
options
)
this.tinymceIntersectionObserver.observe(this.container)
}

/**
* disconnect intersection observer and remove Tinymce editor if the web components get destroyed
*/
disconnectedCallback() {
if (this.tinymceIntersectionObserver !== null) {
this.tinymceIntersectionObserver.disconnect()
}

tinymce.get(this.id)?.remove(this.id)
}

initTinymceEditor() {
const spinner = new Spinner("small")
spinner.spin(this)

// initialize TinyMCE
tinymce.init(this.configuration).then((editors) => {
spinner.stop()

editors.forEach((editor) => {
// mark the editor container as visible
// without these correction the editor remains hidden
// after a drag and drop action
editor.show()

const elementEditor = document
.getElementById(this.id)
.closest(".element-editor")

// event listener to mark the editor as dirty
editor.on("dirty", () => Alchemy.setElementDirty(elementEditor))
editor.on("click", (event) => {
event.target = elementEditor
Alchemy.ElementEditors.onClickElement(event)
})
})
})
}

get configuration() {
const customConfig = {}

// read the attributes on the component and add them as custom configuration
this.getAttributeNames().forEach((attributeName) => {
if (!["class", "id", "is", "name"].includes(attributeName)) {
const config = this.getAttribute(attributeName)
const key = attributeName.replaceAll("-", "_")

try {
customConfig[key] = JSON.parse(config)
} catch (e) {
// also string values as parameter
customConfig[key] = config
}
}
})

return {
...Alchemy.TinymceDefaults,
...customConfig,
locale: Alchemy.locale,
selector: `#${this.id}`
}
}
}

customElements.define("alchemy-tinymce", Tinymce, { extends: "textarea" })
146 changes: 0 additions & 146 deletions app/javascript/alchemy_admin/tinymce.js
Original file line number Diff line number Diff line change
@@ -1,146 +0,0 @@
// Alchemy Tinymce wrapper
//

let tinymceCustomConfigs = {}
let tinymceIntersectionObserver = null

// Returns default config for a tinymce editor.
function getDefaultConfig(editorId) {
const config = Alchemy.TinymceDefaults
config.language = Alchemy.locale
config.selector = `#${editorId}`
config.init_instance_callback = initInstanceCallback
return config
}

// Returns configuration for given custom tinymce editor selector.
//
// It uses the +.getDefaultConfig+ and merges the custom parts.
function getConfig(editorId) {
const editorConfig = tinymceCustomConfigs[editorId] || {}
return { ...getDefaultConfig(editorId), ...editorConfig }
}

// create intersection observer and register textareas to be initialized when
// they are visible
function initEditors(ids) {
initializeIntersectionObserver()

ids.forEach((id) => {
const editorId = `tinymce_${id}`
const textarea = document.getElementById(editorId)

if (textarea) {
tinymceIntersectionObserver.observe(textarea)
} else {
console.warn(`Could not initialize TinyMCE for textarea#${editorId}!`)
}
})
}

// initialize IntersectionObserver
// the observer will initialize Tinymce if the textarea becomes visible
function initializeIntersectionObserver() {
const observerCallback = (entries, observer) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0) {
initTinymceEditor(entry.target)
// disable observer after the Tinymce was initialized
observer.unobserve(entry.target)
}
})
}
const options = {
root: Alchemy.ElementEditors.element_area.get(0),
rootMargin: "0px",
threshold: [0.05]
}

tinymceIntersectionObserver = new IntersectionObserver(
observerCallback,
options
)
}

// Initializes one specific TinyMCE editor
function initTinymceEditor(textarea) {
const editorId = textarea.id
const config = getConfig(editorId)

// remove editor instance, if already initialized
removeEditor(editorId)

if (config) {
const spinner = new Alchemy.Spinner("small")
spinner.spin(textarea.closest(".tinymce_container"))
tinymce.init(config)
} else {
console.warn("No tinymce configuration found for", id)
}
}

// Gets called after an editor instance gets initialized
function initInstanceCallback(editor) {
const element = document.getElementById(editor.id).closest(".element-editor")
element.getElementsByClassName("spinner").item(0).remove()
editor.on("dirty", function () {
Alchemy.setElementDirty(element)
})
editor.on("click", function (event) {
event.target = element
Alchemy.ElementEditors.onClickElement(event)
})
}
tvdeyen marked this conversation as resolved.
Show resolved Hide resolved

function removeEditor(editorId) {
const editorElement = document.getElementById(editorId)
if (tinymceIntersectionObserver && editorElement) {
tinymceIntersectionObserver.unobserve(editorElement)
}

const editor = tinymce.get(editorId)
if (editor) {
editor.remove()
}
}

function removeIntersectionObserver() {
if (tinymceIntersectionObserver !== null) {
tinymceIntersectionObserver.disconnect()
}
}

export default {
// Initializes all TinyMCE editors with given ids
//
// @param ids [Array]
// - Editor ids that should be initialized.
init(ids) {
initEditors(ids)
},

// Initializes TinyMCE editor with given options
initWith(options) {
tinymce.init({ ...Alchemy.TinymceDefaults, ...options })
},

// Removes the TinyMCE editor from given dom ids.
remove(ids) {
ids.forEach((id) => removeEditor(`tinymce_${id}`))
},

// Remove all tinymce instances for given selector
removeFrom(selector) {
// the selector is a jQuery selector - it has to be refactor if we taking care of the calling methods
$(selector).each(function (element) {
removeEditor(element.id)
})
},

removeIntersectionObserver,

// set tinymce configuration for a given selector key
setCustomConfig(key, configuration) {
tinymceCustomConfigs[key] = configuration
}
}
7 changes: 1 addition & 6 deletions app/models/alchemy/ingredients/richtext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,8 @@ def has_tinymce?
true
end

# Returns true if there is a tinymce setting defined that contains settings.
def has_custom_tinymce_config?
custom_tinymce_config.is_a?(Hash)
end

def custom_tinymce_config
settings[:tinymce]
settings[:tinymce] || {}
end

private
Expand Down
1 change: 0 additions & 1 deletion app/views/alchemy/admin/elements/create.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@

Alchemy.growl('<%= Alchemy.t(:successfully_added_element) %>');
Alchemy.closeCurrentDialog();
Alchemy.Tinymce.init(<%= @element.richtext_ingredients_ids.to_json %>);
Alchemy.PreviewWindow.refresh(function() {
Alchemy.ElementEditors.focusElementPreview(<%= @element.id %>);
});
Expand Down
1 change: 0 additions & 1 deletion app/views/alchemy/admin/elements/destroy.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ $('#element_<%= @element.id %>').hide(200, function() {
Alchemy.growl('<%= j @notice %>');
$('#element_area .sortable-elements').sortable('refresh');
Alchemy.PreviewWindow.refresh();
Alchemy.Tinymce.remove(<%= @richtext_ids.to_json %>);
<% if @element.fixed? %>
Alchemy.FixedElements.removeTab(<%= @element.id %>);
<% end %>
Expand Down
Loading