-
-
Notifications
You must be signed in to change notification settings - Fork 315
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
tvdeyen
merged 5 commits into
AlchemyCMS:main
from
sascha-karnatz:update_javascript/migrate-tinymce
Sep 1, 2023
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
2cb6a8e
Migrate Tinymce module into a web component
sascha-karnatz fca5ff6
Add support for attributes on alchemy-tinymce
sascha-karnatz e84fa41
Remove custom id on alchemy-tinymce
sascha-karnatz 675ab92
Allow more complex attributes on alchemy-tinymce
sascha-karnatz 93777f1
Use alchemy-tinymce web component only on textareas
sascha-karnatz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
There was a problem hiding this comment.
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?