Use Plugin Code Editor - Part 3/4: Custom
If you are visiting this page, I suppose you have been encountering these issues I mentioned in my previous post. I am joking. May be or may be not. That's OK.
In this post, I will go through these issues case by case, illustrate what I did to fix or provide a workaround and then explain how I customize Native, Hybrid and Editor to Textarea demo pages.
Click the link in TOC to jump to the section you are interested in or
You probably need to custom your own translation metadata depending on your own locale setting, e.g. you are using Français or 中文.
There is a more logical way to fix this issue by re-calculating width and height of the dialog and Code Editor. Maybe I am too lazy to implement this.
Let's go to detail:
Unfortunately, I have not yet found a solution for this issue but I can offer some workarounds depending on various scenarios.
If you have not yet viewed my previous post Use Theme APEX5.0. Please check the link to see more.
During migration, there are 3 steps below to follow.
Step 1: add Code Editor related CSS and JS files
First from theme Level, copy needed CSS and JavaScript files from Theme APEX5.0 to Theme 42.
The fantastic copy page feature in APEX will prompt for your confirmation to match these templates used in source page to target page. Most of time, I will select them according to the name.
If you can not confirm during coping, you can choose any one first and adjust later in page designer.Please check my demo pages for detailed templates.
And one more thing, for this case we need to copy one label template named APEX 5.0 - Required Label (Above) for Code Editor item type from Theme APEX5.0 to Theme 42. Then set it to item type Code Editor later in page designer.
Step 3: adjust components' CSS attributes
After theme and template level adjustment, we now need to fix the page level rendering issues. Introducing two themes in one page unavoidably causes some conflicts, since we can not prevent Theme 42 related CSS files generation during page rendering. I used page level inline CSS to revise font, rds and button rendering issues after theme merge.
BTW, for Editor to Textarea demo page, you don't need to migrate theme, just use original theme 42.
In this post, I will go through these issues case by case, illustrate what I did to fix or provide a workaround and then explain how I customize Native, Hybrid and Editor to Textarea demo pages.
Click the link in TOC to jump to the section you are interested in or
See more:
- Use Plugin Code Editor - Part 1/4: Demo
- Use Plugin Code Editor - Part 2/4: Intro
- Use Plugin Code Editor - Part 3/4: Custom
- Use Plugin Code Editor - Part 4/4: Retrospect
Fix Issues
Button Title
If you use Code Editor plugin directly, you will get lots of errors logged in console as below for button title/name translation. And from page, you will find these button title letters are upper case.
Format(CODE_EDITOR.SHORTCUT_TITLE): too many arguments. Expecting 0, got 2
In order to fix this issue or provide a workaround, I had to use a dynamic action to re-add message before Code Editor loading and wrote a function to rename these button titles after Code Editor loading.
Basically, when rendering page, APEX engine will prepare these messages for all page components. If you open toolbar of browser(F12) and switch to source tab, you can find a source named wwv_flow.js_messages?p_app_id=xxx&p_lang=en-us&p_version=xxx-xxx. There are three functions in this source file.
Why dynamic action? Because if we want to keep the script execution order, we need run this function before plugin initialization. According to my previous post, we can utilize substitution string #GENERATED_CSS# where DA script goes to, to make sure DA script will be executed before scripts in placeholder #GENERATED_JAVASCRIPT# where plugin initialization goes to.
Adding JavaScript below to page JavaScript (Execute when Page Loads) will help to translate other buttons.
Actually, this script will fix all button title translation as all button titles are included in metadata.apex.lang.addMessages(...); apex.locale.init(...); apex.message.registerTemplates(...);The first function apex.lang.addMessages (click to APEX 5.1 Doc for more information) is used to translate these button titles. In this case, the messages for Code Editor are not included in this function as default. I don't know why but we can simply fix this by adding these message in a dynamic action when page loading.
Why dynamic action? Because if we want to keep the script execution order, we need run this function before plugin initialization. According to my previous post, we can utilize substitution string #GENERATED_CSS# where DA script goes to, to make sure DA script will be executed before scripts in placeholder #GENERATED_JAVASCRIPT# where plugin initialization goes to.
// set message for code editor plugin apex.lang.addMessages({ "CODE_EDITOR.VALIDATION_SUCCESS": "Validation successful", "CODE_EDITOR.UNDO": "Undo", "CODE_EDITOR.REDO": "Redo", "CODE_EDITOR.FIND": "Find", "CODE_EDITOR.FIND_NEXT": "Find Next", "CODE_EDITOR.FIND_PREV": "Find Prev", "CODE_EDITOR.REPLACE": "Replace", "CODE_EDITOR.HINT": "Auto Complete", "CODE_EDITOR.VALIDATE": "Validate", "CODE_EDITOR.SETTINGS": "Settings", "CODE_EDITOR.USE_PLAIN_TEXT_EDITOR": "Use Plain Text Editor", "CODE_EDITOR.SHOW_LINE_NUMBERS": "Show Line Numbers", "CODE_EDITOR.INDENT_WITH_TABS": "Tab Inserts Spaces", "CODE_EDITOR.TAB_SIZE": "Tab Size", "CODE_EDITOR.INDENT_SIZE": "Indent Size", "CODE_EDITOR.THEMES": "Themes", "CODE_EDITOR.SHORTCUT_TITLE": "\u00250 - \u00251", "CODE_EDITOR.SHOW_RULER": "Show Ruler", "CODE_EDITOR.MATCH_CASE": "Match Case", "CODE_EDITOR.MATCH_RE": "Regular Expression", "CODE_EDITOR.CLOSE": "Close", "CODE_EDITOR.FIND_INPUT": "Find", "CODE_EDITOR.REPLACE_INPUT": "Replace", "CODE_EDITOR.REPLACE_ALL": "Replace All", "CODE_EDITOR.REPLACE_SKIP": "Skip", "CODE_EDITOR.QUERY_BUILDER": "Query Builder", });After adding this JavaScript, we can translate some of the buttons for Code Editor, but not all. Then the errors caused by the line 19 highlighted above will prevent translation of other 7 buttons/7 times in toolbar area of Code Editor.
Adding JavaScript below to page JavaScript (Execute when Page Loads) will help to translate other buttons.
// translate button title (function transButtons() { // translation metadata var titleObj = { undo: "Undo - Ctrl+Z", redo: "Redo - Ctrl+Shift+Z", find: "Find - Ctrl+F", replace: "Replace - Ctrl+Shift+F", queryBuilder: "Query Builder", autocomplete: "Auto Complete - Ctrl+Space", settings: "Settings", findNext: "Find Next - Ctrl+G", findPrev: "Find Prev - Ctrl+Shift+G", sClose: "Close", mClose: "Close", FIND_INPUT: "Find", MATCH_CASE: "Match Case", MATCH_RE: "Regular Expression", REPLACE_INPUT: "Replace", REPLACE: "Replace", REPLACE_ALL: "Replace All", REPLACE_SKIP: "Skip", USE_PLAIN_TEXT_EDITOR: "Use Plain Text Editor", INDENT_WITH_TABS: "Tab Inserts Space", TAB_SIZE: "Tab Size", INDENT_SIZE: "Indent Size", THEMES: "Themes", SHOW_LINE_NUMBERS: "Show Line Numbers", SHOW_RULER: "Show Ruler" }; // translate buttons title starting with "CODE_EDITOR." apex.jQuery("[title^='CODE_EDITOR.']") .each(function(i) { var subSource = apex.jQuery(this).attr('id'); var titleStr = titleObj[subSource.substring(subSource.lastIndexOf('_') + 1)]; apex.jQuery(this).prop('title', titleStr).attr('aria-label', titleStr); }); // translate labels with text containing "CODE_EDITOR." apex.jQuery("label:contains('CODE_EDITOR.')") .each(function(i) { var subSource = apex.jQuery(this).text(); apex.jQuery(this).text(titleObj[subSource.substring(subSource.indexOf('.') + 1)]); }); // translate buttons with text containing "CODE_EDITOR." apex.jQuery("button:contains('CODE_EDITOR.')") .each(function(i) { var subSource = apex.jQuery(this).text(); apex.jQuery(this).text(titleObj[subSource.substring(subSource.indexOf('.') + 1)]); }); // translate buttons in pop-menu with text containing "CODE_EDITOR." function menuHandler(event) { var divID = $(this).attr('id'); apex.jQuery(function($) { $('#' + divID + 'Menu') .show(120, function() { $("button:contains('CODE_EDITOR.')").each(function(i) { var subSource = $(this).text(); $(this).text(titleObj[subSource.substring(subSource.indexOf('.') + 1)]); }); }) .show() .css({ 'position': "absolute", 'top': event.pageY, 'left': event.pageX }); }); }; apex.jQuery("button[id$='_widget_settings']") .click(menuHandler); })();
You probably need to custom your own translation metadata depending on your own locale setting, e.g. you are using Français or 中文.
Native
For Native demo pages (including Native - Migrated page ), I used 2 steps above to revise button titles. And it can reduce JavaScript errors but can not fix all. I have not yet figure out why.
Anyway, there errors, if you don't check console, you will not notice them. I mean from usage point of view, you can ignore them. However, from technical point of view, this is an issue.
Hybrid
For two Hybrid demo pages, I removed 53 to 69 lines in the script above because re-generating Code Editor helps to translate buttons in setting menu.
Editor to Textarea
For Editor to Textarea demo page, it's no necessary to include these two scripts above because there is no Code Editor on the page, and no errors.
Maximum Length
When you use item type Code Editor, you might want to define max-length for code input. Unfortunately, you can not. Even though you can set the value for this property, it doesn't work.This is a Code Editor issue, or an un-implemented feature.
Native
In Native demo pages, I used validation binding to item to check the max length. It did work.Hybrid
In Hybrid demo pages, I didn't handle this issue because I suppose all columns are nullable and they are CLOB columns. Actually, you can use some JavaScript to restrict the input length since there is a JavaScript API to getValue of Code Editor I mention in my previous post.Editor to Textarea
In Editor to Textarea demo page, I use maximum length property to restrict the length. It's handy and perfect solution for this issue due to the restriction of input when max-length reaching.Resize
Sometimes, you might want to expand Code Editor to utilize your screen width for better experience, typically in dialog as what I did in my demo.In my previous post, I have mentioned there is a resize event for handling resize in main page but no in dialog. So here this issue is talking about resize in dialog.
Native
Even though there is a resize event defined for _widget div, I prefer to use a stupid way to handle this issue. First, trigger a click to search button, and then set a timeout to click the close button. These two steps will trigger internal resize event of Code Editor.There is a more logical way to fix this issue by re-calculating width and height of the dialog and Code Editor. Maybe I am too lazy to implement this.
Hybrid
Stupid way as above.Editor to Textarea
For Editor to Textarea demo page, I didn't implement expand and restore dialog. And there should be an easy way to handle this issue if you want, just using the class below as what I added in page inline CSS for resizing when navigation menu hide and show.
.t-Form-inputContainer fieldset { width: 100% !important; } textarea { width: inherit !important; }
Validate
Validate is a great feature for Code Editor in APEX. But it's disabled as default and there is no interface provided to enable from page designer.
Native
In Native demo pages, I used validations to check syntax as what I mentioned in my previous post. the only one issue is that error message for SQL mode can not be translated to the detailed info like ORA-xxxx, but it will display as "SQL syntax error" customized by me.Hybrid
In Hybrid demo pages, I used another stupid way which is similar with the way I handled resize issue. OK.Let's go to detail:
- First, set a custom attribute validation="true" for the region which has a Code Editor plugin, which you want to enable validate button in toolbar
- Then, re-generate these Code Editors with validation="true" attribute to enable validate button
- Define a global JavaScript function in page to sniffer the error messages after trigger validate button click event (check Function and Global Variable Declaration property in Hybrid demo Pages)
// use attribute to select plugin with validation button and trigger click apex.jQuery("div[id^='REGION_']").filter("[validation=true]").each(function(i) { var rID = $(this).attr("id"); var wID = rID.substring(rID.indexOf("_") + 1) + "_widget" var vID = wID + "_validate"; // trigger validate button click and bind own click event // after checking unbind own click event apex.jQuery("#" + vID).on("click.myPlugin", function() { var i = setInterval(function() { checkedCounter = 0; validateChecked(wID); if (checkedCounter == 1) { apex.jQuery("#" + vID).off("click.myPlugin"); clearInterval(i); } }, 200); }).trigger("click"); });The code above need to integrate with message handling and page submission in next section to work properly.
Editor to Textarea
As same as Native demo.
Message
If you set error display location to be "inline with Field and in Notification" or "inline with Field" when using validations, you will get error information from browser console log as below.I suppose this issue is related to the third function apex.message.registerTemplates in js file wwv_flow.js_messages I mentioned above. When ajax returning error messages, page wants to render them to a certain div or certain message container, but it can not find the corresponding tag or class in the message templates defined in this function.
Uncaught TypeError: f.addClass is not a function
And the error message will only be displayed in the field, typically below the widget.
Unfortunately, I have not yet found a solution for this issue but I can offer some workarounds depending on various scenarios.
Native
In Native demo pages, I set all validations to return the message to "inline in Notification". This is a great workaround with the benefit that you also can click the message to jump to certain Code Editor.Hybrid
In Hybrid demo pages, I used the following steps to handle messages after trigger validate.- First trigger a click event to all validate buttons
- Then set an interval in JavaScript to check whether the error messages returned to page by ajax call triggered by this click event
- Collect the messages to a message array and cleanup messages in the page
- Then again, set another interval to check whether all messages generated or not
- If yes, then use APEX JavaScript API apex.message.showErrors to show all messages
// define message display location // supported: ["page"], ["inline"] or ["page", "inline"] var messageLocation = ["page"]; // validate and submit page function submitP() { // show all tabs before re-submit, in order to remove all error message before re-validate if ($("div.apex-rds-container li a").filter("[href='#SHOW_ALL']").length == 1) { $("div.apex-rds-container li a").filter("[href='#SHOW_ALL']").trigger("click").blur(); } // remove previous error or warning messages // keeping this order is important! apex.jQuery("button[id$='_widget_mClose']").trigger("click"); apex.jQuery("div.is-error").remove(); apex.jQuery("div.is-warning").remove(); apex.jQuery("li.is-success").remove(); apex.jQuery("div.a-CodeEditor-message").empty(); apex.jQuery("div[id$='_widget_error'").empty(); // remove previous messages and messages stored in Array apex.message.clearErrors(); var messageArr = []; var checkedCounter = 0; var messageCounter = 0; // detect error messages and store to messageArr after click the validate button function validateChecked(widget) { if (apex.jQuery("#" + widget + " div.is-error").length > 0 || apex.jQuery("#" + widget + " div.is-warning") > 0) { checkedCounter = 1; var msg = (apex.jQuery("#" + widget + " div.is-warning").text() == "") ? apex.jQuery("#" + widget + " div.is-error").text() : apex.jQuery("#" + widget + " div.is-warning").text(); // *** generate message for current plugin widget to avoid property value waving among other widgets // during frequent checking poll var eObj = {}; eObj[widget] = { type: "error", location: messageLocation, pageItem: widget, message: msg, unsafe: true }; messageArr.push(eObj[widget]); messageCounter++; } if (apex.jQuery("#" + widget + " .is-success").length > 0) { checkedCounter = 1; messageCounter++; } } // use attribute to select plugin with validation button and trigger click apex.jQuery("div[id^='REGION_']").filter("[validation=true]").each(function(i) { var rID = $(this).attr("id"); var wID = rID.substring(rID.indexOf("_") + 1) + "_widget" var vID = wID + "_validate"; // trigger validate button click and bind own click event // after checking unbind own click event apex.jQuery("#" + vID).on("click.myPlugin", function() { var i = setInterval(function() { checkedCounter = 0; validateChecked(wID); if (checkedCounter == 1) { apex.jQuery("#" + vID).off("click.myPlugin"); clearInterval(i); } }, 200); }).trigger("click"); }); // submit all plugin contents after validation passed function pageSubmit(req) { ... } // set interval to check if validation passed or not // if passed, then submit the page // if no, then show messages var s = setInterval(function() { if (messageCounter == apex.jQuery("div[id^='REGION_']").filter("[validation=true]").length) { if (messageArr.length == 0) { pageSubmit("APPLY_CHANGES"); } else { apex.message.showErrors(messageArr); // fix message click event setTimeout(function() { $("#APEX_ERROR_MESSAGE li a").each(function() { $(this).on("click.myRDS", function() { var wID = $(this).attr("data-for"); $("div.apex-rds-container li a").filter("[href='#REGION_" + wID.substring(0, (wID.length - 7)) + "']").trigger("click").blur(); }); }); }, 500); } clearInterval(s); } }, 200); }
Editor to Textarea
In Editor to Textarea demo page, there is no this issue at all. So no fix needed.
Customize
In this section I will continue to explain what I did in my demo.
Dialog
When we use page designer in APEX, we could find a lot of usage of dialog to expand and restore a Code Editor. Here in my demo, I used jQuery .dialog function to implement this customized feature. Check the code below for detail.
// expand and restore Dialog (function showDialog() { apex.jQuery("button[id$= '_EXPAND']") .click(function() { var expandID = $(this).attr("id"); var itemID = expandID.substring(0, expandID.lastIndexOf("_")); // custom dialog title var itemTitle = $("#REGION_" + itemID + " label.a-Form-label").text(); var sItemTitle = itemTitle.substring(0, itemTitle.lastIndexOf('-') - 1); var s = itemTitle.substring(itemTitle.lastIndexOf('-') + 1); var pItemTitle = s.substring(0, s.lastIndexOf('(') - 1); var editorTitle = '' + ' ' + pItemTitle + ' - ' + sItemTitle; var codeItemWidth = $('#' + itemID + '_widget').width(); var codeItemHeight = $('#' + itemID + '_widget').height(); var dlgWidth = $(window).width() * 0.995; var dlgHeight = $(window).height() * 0.98; $("#" + itemID + "_widget").dialog({ close: function() { $(this).dialog("destroy"); // show scroll bar $("body").css("overflow", ""); // resize Code Editor // *** here trigger click is better than default resize event $(this).css({ height: codeItemHeight + 2, width: codeItemWidth + 2 }); $("#" + itemID + "_widget_find").click(); setTimeout(function() { $("#" + itemID + "_widget_sClose").click(); }, 100); // setTimeout(function() { // $(this).trigger("resize").blur(); // }, 100); $('#' + itemID + '_widget_settings').off("click.myMenuOffset"); }, create: function() { // hide scroll bar $("body").css("overflow", "hidden"); // transfer region title to dialog $(".ui-dialog span.ui-dialog-title").html(editorTitle); // customize restore button $("button.ui-dialog-titlebar-close").removeClass() .addClass("ui-dialog-titlebar-close a-Button a-Button--noLabel a-Button--withIcon a-Button--simple") .css({ "border-radius": "inherit" }) .html(''); // resize Code Editor // *** here trigger click is better than default resize event $("#" + itemID + "_widget_find").click(); setTimeout(function() { $("#" + itemID + "_widget_sClose").click(); }, 100); // setTimeout(function() { // $(this).trigger("resize").blur(); // }, 100); // adjust offset of pop-menu $('#' + itemID + '_widget_settings').on("click.myMenuOffset", function(event) { var menuTop = $(this).offset().top + 32; var menuLeft = $(this).offset().left - 121; $('#' + itemID + '_widget_settingsMenu').one("focusin.myMenuOffset", function(event) { $(this).offset({ 'top': menuTop, 'left': menuLeft, }); }); }); }, hide: "scale", show: "scale", height: dlgHeight, width: dlgWidth, draggable: false, modal: true, resizable: false, closeText: "Restore", overlay: { background: "#000", opacity: 0.15 } }); }); })();
Hybrid: Re-Generate
By default Code Editor doesn't enable validate button in its toolbar area. I used JavaScript to extract original initialization code, re-form and re-generate Code Editors in page, specifically in Hybrid demo page. Check code below for more information.
// re-generate plugin function reGeneratePlugin(widget) { // get script contain ajaxIdentifier var scriptStr = apex.jQuery("script:not([src]):contains('ajaxIdentifier')").text(); // set regexp to match plugin initialization code var re = new RegExp(widget + "'\\,\\s*?\\{[\\S\\s]*?\\}", "ig"); var reArr = scriptStr.match(re); // store original options to object var widgetObj = JSON.parse(reArr[0].substring(reArr[0].indexOf("{"))); var itemID = widget.substring(0, widget.lastIndexOf("_")); // according to region attribute, to enable validate button or not widgetObj["validate"] = (apex.jQuery("#REGION_" + itemID).attr("validation") == "true") ? true : false; // get current app id widgetObj["appId"] = $v("pFlowId"); widgetObj["adjustableHeight"] = (widgetObj["adjustableHeight"] == true) ? true : false; var widgetOriginalWidth = apex.jQuery("#" + widget).width(); var widgetOriginalHeight = apex.jQuery("#" + widget).height(); // remove page elements before re-generation, to avoid duplicate var pa = apex.jQuery("#" + itemID + "_CONTAINER > div.a-Form-inputContainer"); var ch = apex.jQuery("#" + itemID + "_CONTAINER > div.a-Form-inputContainer > div.a-CodeEditor--resizeWrapper"); apex.jQuery("#" + widget).prependTo(pa); ch.remove(); apex.jQuery("#" + widget + "_settingsMenu").remove(); // regenerate apex.builder.plugin.codeEditor("#" + widget, { "adjustableHeight": widgetObj["adjustableHeight"], "mode": widgetObj["mode"], "validate": widgetObj["validate"], "queryBuilder": widgetObj["queryBuilder"], "parsingSchema": widgetObj["parsingSchema"], "readOnly": widgetObj["readOnly"], "settings": widgetObj["settings"], "ajaxIdentifier": widgetObj["ajaxIdentifier"], "appId": widgetObj["appId"] }); // remove duplicate setting menu after re-generate apex.jQuery("#" + widget + "_settingsMenu").remove(); apex.jQuery("#" + widget).css({ height: widgetOriginalHeight + 2, width: widgetOriginalWidth + 2 }); } // set timeout to call re-generate function (function redefinePlugin() { setTimeout(function() { apex.jQuery("div[id$='_widget']").each(function(i) { reGeneratePlugin(apex.jQuery(this).attr("id")); }); transButtons(); }, 500); })();
Migrated: UI
When migrating Native and Hybrid pages from Theme APEX5.0 to Theme 42, the major work is to adjust UI, theme and template.If you have not yet viewed my previous post Use Theme APEX5.0. Please check the link to see more.
During migration, there are 3 steps below to follow.
Step 1: add Code Editor related CSS and JS files
First from theme Level, copy needed CSS and JavaScript files from Theme APEX5.0 to Theme 42.
#IMAGE_PREFIX#apex_ui/js/minified/builder_all.min.js?v=#APEX_VERSION# #IMAGE_PREFIX#sc/sc_core.js?v=#APEX_VERSION# #IMAGE_PREFIX#libraries/raphaeljs/2.1.2/apex.raphael#MIN#.js?v=#APEX_VERSION# #IMAGE_PREFIX#apex_version.js?v=#APEX_VERSION# #THEME_IMAGES#css/Core#MIN#.css?v=#APEX_VERSION# #IMAGE_PREFIX#css/apex_builder#MIN#.css?v=#APEX_VERSION# #IMAGE_PREFIX#css/apex_ui#MIN#.css?v=#APEX_VERSION#From page level, copy another CSS file for Code Editor toolbar rendering. Because this CSS conflict with Theme 42 Core CSS, if put it in Theme level, it will not be emitted by APEX engine and meanwhile we need to keep that its order is after Core CSS generated by Theme 42.
/i/apex_ui/css/Core.min.css?v=5.1.0.00.45Step 2: cope pages and match template
The fantastic copy page feature in APEX will prompt for your confirmation to match these templates used in source page to target page. Most of time, I will select them according to the name.
If you can not confirm during coping, you can choose any one first and adjust later in page designer.Please check my demo pages for detailed templates.
And one more thing, for this case we need to copy one label template named APEX 5.0 - Required Label (Above) for Code Editor item type from Theme APEX5.0 to Theme 42. Then set it to item type Code Editor later in page designer.
Step 3: adjust components' CSS attributes
After theme and template level adjustment, we now need to fix the page level rendering issues. Introducing two themes in one page unavoidably causes some conflicts, since we can not prevent Theme 42 related CSS files generation during page rendering. I used page level inline CSS to revise font, rds and button rendering issues after theme merge.
ul.apex-rds { margin-top: 8px !important; } ul.apex-rds li a { padding: 0px !important; font-size: 13px !important; } ul.apex-rds li.apex-rds-selected a { background-color: #FFF; box-shadow: none !important; } .t-Region-body { font-size: 12px; } .a-Button--listManager, .a-Button--small { font-size: 11px !important; padding: 4px 8px !important; } .t-Region-body.a-Collapsible-content { font-size: 1.4rem; }OK. Now you will have a compatible UI for Code Editor in Theme 42, specifically for Native and Hybrid demo pages.
BTW, for Editor to Textarea demo page, you don't need to migrate theme, just use original theme 42.
I hope I mention all major parts in my demo. Dear readers, if you have any questions, please add to comment below. Thanks.
Comments
Post a Comment