When coding a Gutenberg block plugin for WordPress and you want to remove some of the <RichText> toolbar options on that block. You can set “withoutInteractiveFormatting” and then list the allowedFormats.
I was hitting upon an error in one of my atto plugins whenever I clicked on a button in my dialogue form. It also appears when you use the Atto Styles button on the toolbar.
Uncaught RangeError: Maximum call stack size exceeded.
Using the information given by Jake Dallimore in the comments I added the following to my _displayDialogue function in my YUI module.
require(['core/local/aria/focuslock'], function(FocusLockManager) {
// We don't want to trap focus.
FocusLockManager.untrapFocus();
});
From the comments it seems as though what’s happening is we are ending up with two active focuses when there should just be one. The code seems to work by untrapping the focus when the dialogue is opened so that there is only one again
A few months ago I came across a media query for Reduced Motion in CSS. Some users may find some animated motions on websites can make them feel ill. These users can switch on the Reduced Motion setting in their operating system to try and limit these motions.
As web developers we can help by using CSS (and javaScript) to respect that setting.
CSS Media Query for Reduced Motion
Within the media query you can add settings to prevent transitions or animation from happening.
You can even add a wildcard rules so that any transitions or transforms and animations are prevented as I have in the code below.
/* Tone down the animation to avoid vestibular motion triggers like scaling or panning large objects. */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto; // override smooth scrolling
}
* {
transition: none !important;
animation: none !important;
transform: none !Important;
}
}
Pitfalls
You may find the wildcard approach may disable some transitions or transforms or animations you want to keep so you may have to add some overrides when using that approach or use the css :not notation to ignore certain classes or elements.
/* Tone down the animation to avoid vestibular motion triggers like scaling or panning large objects. */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto; // override smooth scrolling
}
* {
transition: none !important;
animation: none !important;
}
// this ignores the elements and classes specified but targets everything else.
*:not(path, text, g, polyline, rect, svg, clipPath, image, .svg, .mapholder, use) {
transform: none !Important;
}
}
JavaScript Respect Reduced Motion
For JavaScript you can use the following code to check if the user has reduced motion set. Then test the matches – if it returns true then the user has reduced motion set.
var mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
if(mediaQuery.matches) {
$('#'+defid).toggle(); // show and hide rather than...
} else {
$('#'+defid).slideToggle(); // sliding up and down
}
My notes from working out how to build a plugin for Moodle’s Atto Editor. (This post is not entirely finished – working out the dialogs)
Plugin Location
Atto Editor plugins can be found in /lib/editor/atto/plugins as defined here and listed here
Note an Atto Plugin is known as a sub-plugin rather than a full plugin although it follows a similar format.
Required Files
Version.php
/lang/en/atto_pluginname.php
/yui/src/button/
Version.php
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2022030700; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2020061511; // Requires this Moodle version.
$plugin->component = 'atto_pluginname'; // Full name of the plugin (used for diagnostics).
Current Moodle version can be found in Moodle’s own version.php file.
The component name is {plugin_type}_{plugin_name} In this case the plugin type is “atto”
Language String File
This will contain all the translatable language strings for this plugin. It should follow the frankenstyle naming convention of {plugin_type}_{plugin_name}.php i.e. the component name set in version.php
It should at least contain the name of the plugin in there.
$string['pluginname'] = 'Plugin Name';
YUI src button module
This is currently required but looking at the documentation it looks like Moodle are planning on replacing YUI with something else in the future. I worked through the documentation and experimented with it and have recorded more information below.
YUI Modules
Moodle used to use shifter to compile the YUI modules in the past. According to documentation it currently uses Grunt to compile the JS now but the documentation isn’t fully up to date on how this works in practise. I found much more information on how to use shifter so started off with this and will work on shifting to Grunt.
Basic File Structure
/build
This is where the compiled files get stored this is what Moodle actually loads.
/src
/button
/js
button.js (contents / working code for the module)
/meta
button.json (list of dependencies for the module)
build.json (essentially instructions for shifter or grunt on how to build the final js file)
Note the name “moodle-atto_pluginname-button” in “Builds” indicates the name of the folder it compiles to. The “Name” indicates the name of the javascript files after compiling.
Moodle Documentation has a list of examples for this file. Update the examples to use your component and plugin name. e.g. in the atto_strike example you would replace “atto_strike” with “atto_pluginname”
Compiling with Shifter
This is the old way of compiling the YUI module and reading through the threads on it, YUI will eventually be replaced but that may be sometime away yet due to the complexity. I’m going to take a look at compiling with Grunt as well but found more information on shifter so looked at it initially as a temporary way of compiling the YUI module.
To use shifter you need to have node.js installed then install shifter.
npm -g install shifter
Once installed navigate to your YUI /src folder and run the shifter watch command.
cd ../yui/src/
shifter --watch
Any change you make in button.js after that will cause the compiler to recompile all the files into the build folder.
Button Icon Locations
Icons need to be stored in /pix/filename within the plugin. SVG is supported 😊 They can then be referenced in button.js when creating a button – example below:
The icon name “e/bootstrap” is referring to an image stored at /pix/e/bootstrap.svg inside the plugin. The iconComponent name is the plugin component name so it know to look in /lib/editor/atto/plugins/atto_pluginname The title “pluginname” is reference to a language string in /lang/en/atto_pluginname.php
Experimenting with Button.js
I’ve added a few examples of what you can do in the YUI module. I learned a lot of this looking at the code for Core Moodle Atto plugins. A good one to check out is the atto_table plugin.
Using Language Strings in Button.js
You can reference language strings in your YUI module with the following reference:
M.util.get_string("stringReference", component)
Replace “stringReference” with the name of your string located in /lang/en/atto_pluginname.php
The variable component should be declared somewhere in the code and be the component name of your plugin e.g. atto_pluginname
The string should also be listed in the lib.php file for the plugin in the function atto_pluginname_strings_for_js (replace atto_pluginname with your plugin component name).
function atto_pluginname_strings_for_js() {
global $PAGE;
$PAGE->requires->strings_for_js(array('stringReference'),
'atto_pluginname');
}
If using mustache templates you can also reference these strings using the following reference and passing the variable via javaScript
{{get_string "stringReference" component}}
var component = 'atto_pluginname',
EXAMPLETEMPLATE = '<div>{{get_string "stringReference" component}}</div>';
You can then use the below code to convert the mustache strings in the example template to the Language strings stored in your plugin language file ( /lang/en/atto_pluginname.php ). Remember to pass the component name (usually stored in a variable) and to reference the string in the plugin’s lib.php file otherwise it won’t work!
/* This example function can pass form content to a dialogue box in Atto */
_getDialogueContent: function() {
// this will be the form template.
var finaltemplate = Y.Handlebars.compile(EXAMPLETEMPLATE);
// insert variables into the template
var content = Y.Node.create(finaltemplate({
component: component
}));
return content;
},
Inserting a Template into Content Example
/**
* Change the title to the specified style.
*
* @method _insertTemplate
* @param {EventFacade} e
* @param {string} template
* @private
*/
_insertTemplate: function(e, template) {
this._currentSelection = this.get('host').getSelection();
var host = this.get('host');
// Focus on the last point.
host.setSelection(this._currentSelection);
// And add the template. e.g. <p>test</p>
host.insertContentAtFocusPoint(template);
// Mark as updated
this.markUpdated();
}
A callback that can be added to globalItemConfig: { } to insert a HTML template into the main content editor. This can be extended to fill in information from a dialog form.
Note: I think there is probably a way to compile the template passed in like there is for the form dialogue but I ended up writing a custom one for inserting into Editor Content because if I use the same method as form dialogue it goes wrong – I’m suspecting I don’t have a setting right somewhere but I need to look deeper into this!
/**
* Replace Mustache Placeholders with content
*
* @method _replaceMustache
* @param {string} template (html)
* @param {object} variables
* @private
*/
_replaceMustache: function(template, variables) {
// holder for the final output.
var finalHtml = template;
// this next bit is only if we have variables to play with.
if(typeof variables == "object" && Object.keys(variables).length !== 0) {
// Loop through the object
for (const [key, value] of Object.entries(variables)) {
// find each mustache key and replace with the value.
var regex = new RegExp(`{{${key}}}`,`g`);
finalHtml = finalHtml.replace(regex, value);
}
}
return finalHtml;
},
Troubleshooting
A few things that tripped me up while working out how to get this to work!
Button not appearing
Check it has been set in Site Administration > Plugins > Text Editors > Atto HTML Editor -> Atto Toolbar Settings ( ../admin/settings.php?section=editorsettingsatto )
Add “pluginname = pluginname” to the bottom of Toolbar Config to make it show at the end of the toolbar. You can also insert it inside a group of icons if needed. Note you should change “pluginname” to match your component name
Question for later : is it possible to add it by default or is that a bad idea and if so why?
Missing Strings?
If a string is called in the YUI js files it must be specified in the language strings file in order to show. Otherwise you will get errors!
It also may need to be specified in lib.php in the atto_pluginname_strings_for_js() function.
function atto_pluginname_strings_for_js() {
global $PAGE;
$PAGE->requires->strings_for_js(array('accordion',
'pluginname',
'h3',
'h4',
'h5',
'pre',
'p'),
'atto_pluginname');
}
Missing YUI component?
This seems very obvious in hindsight but although the documentation only mentions requiring yui/src/button.js you actually need javascript files in /yui/build/moodle-atto_pluginname-button/ to exist for example : moodle-atto_pluginname-button.js as well for this to work. Otherwise it just won’t load and you’ll get a missing module message in dev tools console. These files get automatically created when you compile with shifter or (presumably) Grunt.
YUI module not compiling
One of those silly obvious ones but make sure any custom functions you add are separated by a comma!
Due to the unprecedented circumstances this year the College had to host their Open Day virtually.
We wanted to have a live feed of heads of departments answering questions which we got during the feed and beforehand. Because we wanted this front and centre we had to look into a way of embedding the feed into our dedicated Open Day webpage.
After looking around online I came across this interesting discussion on embedding Live YouTube Feeds and used it as a basis to create my own class.
<?php
/*****************************************************************
Class : LIVE EVENT SCHEDULER
This class handles the live event schedule and loads the
youtube live feed if an event is due.
FUNCTIONS
---------------------------------------------------------
1. showAll()
show all scheduled events as cards below the main feed
2. getNextEvent()
get an array of the next event's details
3. showAllUpcoming()
display a mini feed of all the upcoming events.
4. showAllPast()
5. getLiveChatURL()
6. getLiveVideoID($channelId)
7. fetchVideoFeeds()
*****************************************************************/
class Schedule {
public $schedule = array(
array(
'datetime' => '202006121100',
'show' => false, // show in main schedule
'subject' => 'Welcome Message',
'speaker' => 'Principal & Vice Principal ',
'img' => '',
'vid' => '',
'desc' => 'Welcome message from the Principal.',
'link' => array(
array( 'link' => '', 'text' => 'Find out more' )),
),
);
// Properties
public $date;
public $time;
public $speaker;
public $subject;
function __construct() {
}
/****************************************
show all upcoming scheduled items.
These appear as cards below the main feed.
@RETURNS
$html (string) : Compiled HTML
****************************************/
public function showAll() {
// Display code goes here.....
return $html;
}
/*********************************************
Get the next event
This was used to help calculate when the next event was.
@RETURNS
$return (array) : The event array that is next up.
*********************************************/
public function getNextEvent() {
$now = date_create(); // get current date and time.
$compare = date_format($now,"YmdHis"); // formatted current date and time to use as a comparison
$return = $this->schedule[0]; // The event array that is next up. (default as first)
// loop through events and compare with formatted date.
foreach($this->schedule as $event) {
if($compare < $event['datetime'].'00') {
$return = $event;
break;
}
}
return $return;
}
/***************************************************
Show all upcoming events as a mini feed
@RETURNS
$html (string) : Container for the compiled HTML
***************************************************/
public function showAllUpcoming() {
// Display code goes here ....
return $html;
}
/***********************************************
Show all past events
added so that past videos could be viewed.
@RETURNS
$html (string) : Compiled HTML of past events.
***********************************************/
public function showAllPast() {
// Display Code goes here....
return $html;
}
/********************************************
Fetch Live Chat URL from YouTube Channel
This requires a YouTube Channel ID.
It will try to fetch the current live video feed ID
present on that Youtube Channel.
If it can't find any Live Feeds will return an error.
If it can the live chat feed url is returned.
*********************************************/
public function getLiveChatURL() {
try {
$livevideoId = $this->getLiveVideoID('<< REPLACE WITH CHANNEL ID >>');
// Output the Chat URL
//echo "The Chat URL is https://www.youtube.com/live_chat?v=".$livevideoId;
return array( true, "https://www.youtube.com/live_chat?v=".$livevideoId);
} catch(Exception $e) {
// Echo the generated error
return array( false, "1. ERROR: ".$e->getMessage());
}
}
// The method which finds the video ID
public function getLiveVideoID($channelId)
{
$livevideoId = null;
// Fetch the livestream page
if($data = file_get_contents('https://www.youtube.com/embed/live_stream?channel='.$channelId))
{
// Find the video ID in there
if(preg_match('/\'VIDEO_ID\': \"(.*?)\"/', $data, $matches))
$videoId = $matches[1];
else
throw new Exception('2. Couldn\'t find video ID');
}
else
throw new Exception('3. Couldn\'t fetch data');
return $videoId;
}
// AJax function.
public function fetchVideoFeeds() {
$html = '';
$result = $this->getLiveChatURL();
$html .= '<div class="col-xs-12 col-lg-8">';
//$html .= '<pre>Dev Note : We are testing the live stream right now</pre><iframe style="width: 100%; height: 411px;" src="https://www.youtube.com/embed/live_stream?channel=CHANNEL_ID_HERE&autoplay=1&rel=0&showinfo=0&cc_load_policy=1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen autoplay></iframe>';
if($result[0] == true) {
$html .= '<iframe title="VIDEO FEED TITLE" aria-label="VIDEO ARIA LABEL" style="width: 100%; height: 411px;" src="https://www.youtube.com/embed/live_stream?channel=CHANNEL_ID_HERE&autoplay=1&rel=0&showinfo=0&cc_load_policy=1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen autoplay></iframe>';
} else {
$html .= '<iframe title="VIDEO TITLE" aria-label="VIDEO ARIA LABEL" style="width: 100%; height: 411px;" src="https://www.youtube-nocookie.com/embed/ZvzgjA5hhdo?rel=0&autoplay=0&rel=0&showinfo=0&cc_load_policy=1" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>';
}
$html .= '</div>
<div class="col-xs-12 col-lg-4">';
if($result[0] == true) {
$html .= '<iframe src="'.$result[1].'&embed_domain=www.YOURDOMAIN.ac.uk" style="width: 100%; height: 411px" frameborder="0"></iframe>';
} else {
$html .= '<div class="nofeed">';
$html .= '<h2 class="arrow">Upcoming Events </h2>';
$nextEvent = $this->getNextEvent();
$datetime = date_create($nextEvent['datetime']);
$today = date_create();
$dateDiff = date_diff($today,$datetime);
$html .= '<div class="content">';
$html .= '<div class="countdown">';
$html .= '<div style="display: none;">'.($dateDiff->d > 0 ? $dateDiff->d.'d ' : '').(($dateDiff->d > 0 || $dateDiff->h) > 0 ? $dateDiff->h.'h ' : ''). (($dateDiff->h || $dateDiff->i > 0) ? $dateDiff->i.'m ' : '').$dateDiff->s.'s</div>';
$html .= '<h6>The next event will be in <time class="counter" datetime="'.date_format($datetime,'Y-m-d\TH:i:s\Z').'" data-now="'.date_format($today, 'Y-m-d\TH:i:s\Z').'">'.($dateDiff->d > 0 ? $dateDiff->d.'d ' : '').(($dateDiff->d > 0 || $dateDiff->h) > 0 ? $dateDiff->h.'h ' : ''). (($dateDiff->h || $dateDiff->i > 0) ? $dateDiff->i.'m ' : '').$dateDiff->s.'s</time></h6>';
$html .= $this->showAllUpcoming();
$html .= '</div>';
$html .= '</div>';
$html .= '</div>';
}
$html .= '</div>';
return $html;
}
}
// init
$schedule = new Schedule();
?>
(I’ve taken out some of the irrelevant display bits).
The Schedule
For this example I have the schedule as an array but really it would be better to use a database and add some fetch from database functions to the class.
showAll(), showAllUpcoming(),showAllPast()
These functions just displayed all the upcoming events below the feed and aren’t important for this post.
getNextEvent()
We had a little countdown running on the page to the next event this function was just to help with that countdown and not important for this post.
getLiveChatURL()
This function tries to fetch the Live Chat url if it fails it will return an error message.
To get the Live Chat URL we also need the Live Video ID. This is fetched via the next function getLiveVideoID() . It requires the Channel ID to get this.
Getting the Channel ID
It’s actually fairly straightforward to get the Channel ID. You can either get this from your channel settings or you can simply go to your channel and the ID will display at the end of your URL.
the ID is the string of characters after /channel/
Note About YouTube Requirements
Please note there are a few requirements regarding embedding Live Video Feeds.
To stream direct to Youtube feeds must be public and allow embedding for the script to work.
You need a certain number of subscribers (1000+) and be eligible for the YouTube Partner Program to be able to embed youtube feeds on your websites directly.
Had we known! Restream.io costs but you can go live on multiple platforms at once (Twitter/Twitch/Facebook/Instagram) and embed on any website you like. ( thanks Steve 🙂 )
The Live Chat Feed for any Live Video will follow the above format. Of course this might change in the future with YouTube updates but this works at time of writing.
/********************************************
Fetch Live Chat URL from YouTube Channel
This requires a YouTube Channel ID.
It will try to fetch the current live video feed ID
present on that Youtube Channel.
If it can't find any Live Feeds will return an error.
If it can the live chat feed url is returned.
*********************************************/
public function getLiveChatURL() {
try {
$livevideoId = $this->getLiveVideoID('PASTE CHANNEL ID HERE');
// Output the Chat URL
//echo "The Chat URL is https://www.youtube.com/live_chat?v=".$livevideoId;
return array( true, "https://www.youtube.com/live_chat?v=".$livevideoId);
} catch(Exception $e) {
// Echo the generated error
return array( false, "1. ERROR: ".$e->getMessage());
}
}
getLiveVideoID($channelId)
This function gets the ID of the Live Video using the provided Channel ID.
Currently the Stream url will follow the above format. Of course this might change in the future with YouTube updated but this works at time of writing.
if(preg_match('/\'VIDEO_ID\': \"(.*?)\"/', $data, $matches))
$videoId = $matches[1];
else
throw new Exception('2. Couldn\'t find video ID');
The script then searches the returned file contents for the ‘VIDEO_ID’ variable which is printed in the javaScript on that url.
^ There may be another way of doing this via YouTube’s Javascript Api but this does work.
// The method which finds the video ID
public function getLiveVideoID($channelId)
{
$livevideoId = null;
// Fetch the livestream page
if($data = file_get_contents('https://www.youtube.com/embed/live_stream?channel='.$channelId))
{
// Find the video ID in there
if(preg_match('/\'VIDEO_ID\': \"(.*?)\"/', $data, $matches))
$videoId = $matches[1];
else
throw new Exception('2. Couldn\'t find video ID');
}
else
throw new Exception('3. Couldn\'t fetch data');
return $videoId;
}
fetchVideoFeeds()
Finally I added a function to display the final feed or schedule if there was no live feed. This can also be requested via Ajax to update the feed automatically once the countdown has been reached.
$result = $this->getLiveChatURL();
First, this function gets the current Live Chat result from the above function. If there’s no result from this e.g. an error the script instead shows an advert for the college and a schedule of upcoming Live Events.
Screenshot of the upcoming Events Schedule.
If there is a result the script replaces the video advert with the live feed…
I also added in a little countdown which is updated by javaScript.
All this html is then returned by the function.
Initialising the Class
// init
$schedule = new Schedule();
Final part of the class.php file it creates a new instance of the class.
The Open Day Page
On the open day page where the feed is to be displayed I included the new class file.
<?php
// Include the Event Scheduler Class. (handles the schedules and live video links)
include('classes/class_schedule.php');
?>
And added in an area for the Live Feed to go…
<!-- LIVE VIDEO FEEDS & CHAT -->
<a class="pastevents" href="#eventspast" title="View Past Events" aria-label="View Past Events">Watch Past Events</a>
<div id="videofeed" class="row videofeeds">
<?php echo $schedule->fetchVideoFeeds(); ?>
</div>
<!-- END OF LIVE VIDEO FEEDS AND CHAT -->
JavaScript Countdown
I also added in a little bit of javaScript that updates the countdown and reloads the feed area once the countdown hits 0.
jQuery(document).ready( function($) {
// Set the date we're counting down to
var nextEventDate = $('.counter').attr('datetime');
var today = $('.counter').data('now');
var countDownDate = new Date(nextEventDate).getTime();
// Update the count down every 1 second
var x = setInterval(function() {
// Get today's date and time
var today = new Date();
// DST
today.setHours(today.getHours() + 1);
var now = today.getTime();
// Find the distance between now and the count down date
var distance = countDownDate - now;
// Time calculations for days, hours, minutes and seconds
var days = Math.floor(distance / (1000 * 60 * 60 * 24));
var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
// Display the result in the element with id="demo"
$('.counter').html((days > 0 ? days + "d " : '') + ((days > 0 || hours > 0) ? hours + "h " : '') + ((hours > 0 || minutes > 0) ? minutes + "m " : '') + seconds + "s ");
// If the count down is finished, write some text
if (distance < 0) {
clearInterval(x);
$('.counter').html('Now....reloading in <span class="reloadcount">3</span>');
// fetch the feed...
$.get("fetch.php", function(data, status){
$('.videofeeds').html(data); //.animate({'opacity' : '1'}); // fade back in.
});
}
}, 1000); //testing
});
Notes about Feed Refresh
There is roughly a minute delay between when you start your Live Feed and it appearing on YouTube so it’s a good idea to have a buffer time and start the Live Feed before the event is scheduled. This also mitigates any issues with server times being slightly out of sync.
We had a 15 minute buffer period before each event.
fetch.php
The fetch.php file just includes the schedule class and echos out the feed.
<?php
// Include the Event Scheduler Class. (handles the schedules and live video links)
include('classes/class_schedule.php');
echo $schedule->fetchVideoFeeds();
?>
I am currently learning and updating this post as I go along so it may change over the next few weeks 🙂 .
XD has this cool little area for adding in plugins to your install. They can be anything from inserting useful elements like calendars, to testing your content is accessible for those with colourblindness.
Then I suddenly noticed a little tab called “Development” under plugins:
So as a challenge I’ve decided to have a go at making my own plugin for XD and make a blog post about it.
Notes
XD plugins use JSON and JavaScript – so not too much of a leap!
A basic XD plugin consists of a manifest.json file and a main.js file. You can also add images and other files but those two files are all that is really needed.
I also then split the files so there was a distribution folder as I don’t want to include development files in the final version that gets moved to Adobe XD.
I’ve documented that in this post here. Following those steps you end up with:
Once the plugin is finished it is the example-plugin folder that is copied into the Adobe XD develop folder.
Finally I can update the package.json file. Note : “private”: true means that it can’t be published by accident 🙂
There is a console you can use for catching error messages. Go to Plugins -> Development -> Developer Console
Setting a Task / Brief
I generally learn more when I’m trying to do something. So now I’ve got the Quick Start Tutorial working and gone through some of the introduction tutorials + the Quick Start React Tutorial. I’m going to set myself a simple task to learn more.
I’m going to create a Lorem Ipsum generator (I know these already exist but I thought I would start with something that appeared to be simple then expand on it. Plus once I’ve got it working I can try bringing in some more fun Lorem Ipsum Generators rather than the boring standard one).
Requirements
Insert Lorem Ipsum into a rectangle
Fetch the Lorem Ipsum from a file
Get the last selected font settings as default
Allow user to change default font settings.
Challenge Requirements
Allow user to choose type of Lorem Ipsum.
Allow user to change the settings on how much lorem ipsum they need, paragraphs, lines, words e.t.c.
I then changed the first line of the main.js to include the text scenegraph element.
const { Rectangle, Text, Color } = require("scenegraph");
I replaced the “enlargeRectangle” function in the create function with my own function called “replaceWithLoremIpsum”
function replaceWithLoremIpsum() {
const { editDocument } = require("application"); // [3]
let coords;
// Edit the Document...
editDocument({ editLabel: "Insert Lorem Ipsum." }, function(selection) {
// Currently Selected Rectangle.
const selectedRectangle = selection.items[0]; // [7]
// Get the Rectangle's Coordinates.
coords = selectedRectangle.boundsInParent;
// Create a new Text Node
const node = new Text();
// Text Settings
node.text = "Lorem Ipsum will go here...";
node.fill = new Color("#1f1f1f");
node.fontSize = 50;
// Match the selected Rectangles area and location.
node.areaBox = {width: coords.width, height: coords.height};
node.translation = {x: coords.x, y: coords.y };
// add the new text box.
selection.insertionParent.addChild(node);
// Hide the selected rectangle (keeping it in case the user still needs it.
selectedRectangle.visible = false;
});
}
This function takes the currently selected rectangle, records the coordinates and dimensions of the rectangle, creates a new Text node, Adds some text to the new text node, Sets some text settings, Changes the Text node to a fixed width and height that matches the selected rectangle (otherwise it just sets an auto height & width), Moves the Text area to the same coordinates as the selected rectangle, Add the new text node to the artboard, Hides the selected rectangle from the user view.
I decided to keep the rectangle rather than removing it incase the user still needs it. They can always delete it if they prefer.
I then updated the querySelector to use my new function rather than the enlargeRectangle function.
I’ve created a text folder in the plugin directory with a file called latin.json that has the Lorem Ipsum reference text in it.
{
"content" : "Lorem ipsum dolor sit amet, conse",
}
I added a variable near the top to act as a container for the selected lorem ipsum.
let textToAdd = "Lorem Ipsum will go here..."; // Container for the Lorem Ipsum text to add...
I added a new function to the create function to handle the generation of the Lorem Ipsum text. I’ve written it in this way as I’m intending to expand on it later.
/**************************************************************
FUNCTION TO FETCH LOREM IPSUM
**************************************************************/
function fetchLoremIpsum() {
const noScentences = 7; // Total number of scentences to include in a paragraph
const noParagraphs = 2; // Total number of paragraphs to include
// Fetch the lorem ipsum.
const latin = require("./text/latin.json");
// Split text into scentences
let scentences = latin.content.split('.');
// Clear any existing text
textToAdd = '';
// keep adding scentences so long as we have enough scentences per paragraph.
for(let i = 0; i < (noScentences * noParagraphs); i++) {
textToAdd += scentences[i]+'.';
// if we've reached the total number of scentences per paragraph
if(i > 0 && (i % noScentences) === 0) {
textToAdd += '\n'; // add a line break.
}
}
}
Then I change the new text node to use the textToAdd variable rather than a set string.
node.text = textToAdd;
Get the last selected font settings as default
I initially wasn’t sure if this would be possible but I thought there probably would be a way to select existing nodes in the document without the user indicating one.
I added a new container variable for the last submitted text node settings at the top of main.js
let lastSubmittedText; // Container for Last Submitted Text Node
I then created a new function in the create function called findTextSettings.
/**************************************************************
FUNCTION TO FIND PREVIOUS TEXT SETTINGS
Parameters that are passed down are Contextual Arguments
( https://adobexdplatform.com/plugin-docs/reference/structure/handlers.html#contextual-arguments )
**************************************************************/
function findTextSettings(selection, documentRoot) {
// for each node in the document....
documentRoot.children.forEach(node => {
// Check if node is a child of Artboard
if(node instanceof Artboard) {
let artboard = node;
// Filter out the child nodes that are Text nodes and set as text
let text = artboard.children.filter(artboardChild => {
return artboardChild instanceof Text;
});
let lastText = text[(text.length - 1)];
// Set last submitted Text settings.
lastSubmittedText = {
fill : lastText.fill,
fontSize : lastText.fontSize
};
}
});
}
I modified the example function for selecting rectangles in the artboard to select text nodes instead and to update the container variable with the new data.
Note about Contextual Arguments: https://adobexdplatform.com/plugin-docs/reference/structure/handlers.html#contextual-arguments
The documentRoot parameter needs to be added to the editDcoument function for this to be passed down.
There are more settings than this so I am now going through and adding all the rest of the settings.
Allow user to change default font settings.
Although fetching the last selected settings is useful as a fallback, I think the user should be able to change the text settings before they import the Lorem Ipsum.
This means adding in some inputs to the main form.
This post is a work in progress and is mainly documentation on things I have discovered / learned while working on a project. There are probably better ways of doing some of this.
Some uploader settings
dropzone : This is the container element where the user will be dropping files. This triggers the drop files here when files are dragged over the element.
container : To confirm
browser : To confirm
error : Error handler e.g. function(e){ }
success : This fires when a file has been uploaded (it fires for each file that has been added).
added : This fires as each file is added to the drop zone (it fires for each file added).
progress : This fires while the file is being uploaded, (it fires for each file that has been added).
complete : I’ve never seen this fire so far so I’m not sure what triggers it.
refresh : I think this fires if the instance is refreshed but I haven’t confirmed.
uploader: {
dropzone: $('.dragzone'), // The dropzone container
container: $('.dragzone'),
browser: 'browse_button',
error: function(e) { // if there is an error during upload this should fire.
console.log(e);
console.log('error');
},
success: function(e) { // once a file is uploaded this should fire.
//console.log(e);
//console.log('success');
fileTemplate(e);
noItemsUp++; // update the number of items uploaded.
prevProgress = Math.floor(progressBar);
progressBar = Math.floor(percent * noItemsUp);
animateProgress();
$('#importFiles').prop('disabled', false);
},
added: function(e) { // this fires when a file is added.
noItems++; // add 1 to the number of items being uploaded
percent = 100 / noItems; // work out the new percentage per item...
prevProgress = progressBar; // This is needed so the animated counter can work
progressBar = percent * noItemsUp; // This is the new progress the bar needs to move to.
animateProgress();
},
progress: function(e) { // this fires while the file is being uploaded.
//console.log(e);
//console.log('progress');
},
complete: function(e) { // ?
//console.log(e);
//console.log('complete');
},
refresh: function(e) { // ?
//console.log(e);
//console.log('refresh');
}
}
The task : create a single file upload field on the add / edit page of a custom post type within a WordPress plugin. The file should use the WordPress media library functionality to upload and select the file.
I’ve added some screenshots of the finished field. I’ve added the field into the “Add New Custom Post Type” form using a meta box.
When the “Add the File” button is clicked (or the input field) the WordPress media library modal should appear.
I’ve modified the parameters to change the title of the modal window and the text on the button.
Once a file has been selected and chosen the information in the file preview box is filled out:
The Code
To use the WordPress Media api you need to include the media library scripts so that your custom script can access its functionality. To do this you will need to create a function for “enqueue”-ing scripts to your WordPress admin. Mine is created as part of a class for my plugin:
// Set some constants
defined( 'FMPLUGINURL' ) or define( 'FMPLUGINURL', plugin_dir_url( __FILE__ ) );
class FileManager {
// Initialise the plugin
public static function init() {
add_action('admin_enqueue_scripts',['FileManager','queue_assets']);
}
// Add your assets to the admin view.
public static function queue_assets() {
// Checks that it's in the admin view
if(is_admin()) {
wp_enqueue_media(); // WP Media API
wp_register_script( 'fm_media_uploader', FMPLUGINURL.'assets/js/fm_media_uploader.js'); // Customised WP media Upload Script for this plugin.
wp_localize_script( 'fm_media_uploader', 'FMPLUGINURL', FMPLUGINURL );
wp_enqueue_script('fm_media_uploader');
}
}
}
FileManager::init();
The above code adds all the assets necessary for using the media JavaScript API.
The fm_media_uploader file is where I am adding all the custom JavaScript to hook into the media API.
fm_media_uploader.js
First we need to setup an instance of the Media Library Modal which I will be storing in the variable below.
// Set some required variables
var mediaUploader;
We also will need a trigger so that the Media Library Modal will open when certain elements are clicked.
// Set some required variables
var mediaUploader;
// Set the triggers
$('#mediaUpload, .file-field input, .file-options a').on("click", function(e) {
// This just prevents any default functions from occuring e.g. stops a form from submitting.
e.preventDefault();
});
The instance of the media uploader will be created once the trigger is clicked.
// Set the media uploader with the following parameters
mediaUploader = wp.media.frames.file_frame = wp.media({
title: 'Choose File', // Title at the top of the Modal Window
button: { // Button Parameters
text: 'Choose File', // Button Text
},
multiple: false // Allow multiple file uploads?
});
This sets up an instance of the Media Library Modal while modifying the title of the window and the button parameter. There is also an option to upload multiple or single files. There may be more parameters but these are the ones I know about so far. I may add to this post (and my next one) as I learn more.
This is how the modal window will look once triggered.
Finally we will tell the modal window to open:
// open the Media Uploader.
mediaUploader.open();
So once the Add File button is clicked the WordPress Media Library modal window will appear and allow you to select a file. However you may find two problems:
You can’t open the same modal window a second time.
Nothing happens to the file once it is selected.
Prevent Multiple Instances
The reason you might have problems clicking the button a second time is that it is trying to create multiple instances on the same variable. So we need to add some code so the creation part is skipped if the instance has already been created.
if (mediaUploader) {
// open the media uploader.
mediaUploader.open();
return;
}
If an instance of the Media Library Modal already exists it simply opens to existing instance and skips the part tht creates the instance.
I think this part could be cleaner. It is something I’m planning on revisiting later.
Handling the Selected File
The following piece of code handles the selected file and should be added when the instance of the Media Library Modal is created but before it is opened.
I may rewrite this bit in the future, as I think it could be neater.
// When a file is selected in the modal window....
mediaUploader.on('select', function() {
var attachment = mediaUploader.state().get('selection').first().toJSON(); // the selected file is added to the selection list
var fileDescription = $('textarea[name="FMDescription"]').val(); // get current File Description
var file = attachment.filename.split('.'); // get the filename and split into an array.
file.reverse(); // reverse the array to make it easier to get the file extension
$('.file-type').text('.'+file[0]); // File Extension
$('input[name="FMFile"]').val(attachment.url); // File URL
$('.file-header').text(attachment.title); // File Title
if(fileDescription == '') {
$('textarea[name="FMDescription"]').val(attachment.description);
} // Set File Description if nothing has been filled in yet...
$('.upload-date').text(attachment.dateFormatted); // File Upload Date Formatted (this will use WordPress's default setting)
$('.file-size').text(attachment.filesizeHumanReadable); // File Size
$('.file-options .edit').attr('href', attachment.editLink).text('Edit');
$('input[name="FM_attachment_id"]').val(attachment.id); // Attachment (file) ID
var date = $('#jquery-datepicker').val();
if(date) {
$('input[name="FMTitle"]').val(attachment.title+' '+date);
}
});
This may have more code than you need as I’ve taken it from my own project.
First we have an event listener that runs a function when the ‘select’ event is triggered on the Media Library Modal instance.
mediaUploader.on('select', function() { });
Then we need to collect all the details of the file that has been selected. This is returned in JSON format.
var attachment = mediaUploader.state().get('selection').first().toJSON();
The rest of the code is just me telling the script to fill in various fields with the file data to display it like below:
Handling Edit Mode
When the custom post type is reopened in edit mode; it should automatically have the file selected when the file field is clicked. However at present the script won’t let this happen. We need to add some additional code to make this happen.
var fileID = $('input[name="FM_attachment_id"').val();
First we grab the attachment ID (I use a hidden field to store this in the Meta Box).
Then in both the open existing mediaUploader statement and the mediaUploader.on(‘open’, function()) we need to add the following:
if(fileID) {
// select the file ID to show it as selected in the Media Library Modal.
mediaUploader.uploader.uploader.param( 'post_id', parseInt(fileID) );
var selection = mediaUploader.state().get('selection');
selection.add(wp.media.attachment(fileID));
}
It needs to be added twice because you might try and edit after you’ve added a file or when you are opening an existing custom post type. So it needs to check both in the creation of the instance and when an existing instance is opened.
I’m planning on working on this to try and make it neater and not repeat the same code twice but hey it works for now.
The Full Code
This is the current javaScript code in full…
// Set some required variables
var mediaUploader;
// Set the triggers
$('#mediaUpload, .file-field input, .file-options a').on("click", function(e) {
// This just prevents any default functions from occuring e.g. stops a form from submitting.
e.preventDefault();
// Fetch the value of the currently selected File - if nothing has been selected this will be empty
var fileID = $('input[name="FM_attachment_id"').val();
// If there is already a Media Library Modal only this block will run as it doesn't need to be initialised again.
if (mediaUploader) {
// This should only be run if a file has already been selected
if(fileID) {
// when the media library modal is opened....
mediaUploader.on('open', function() {
// if there's a file ID
if(fileID) {
// select the file ID to show it as selected in the Media Library Modal.
mediaUploader.uploader.uploader.param( 'post_id', parseInt(fileID) );
var selection = mediaUploader.state().get('selection');
selection.add(wp.media.attachment(fileID));
}
});
}
// open the media uploader.
mediaUploader.open();
return;
}
// Set the media uploader with the followign parameters
mediaUploader = wp.media.frames.file_frame = wp.media({
title: 'Choose File', // Title at the top of the Modal Window
button: { // Button Parameters
text: 'Choose File', // Button Text
},
multiple: false // Allow multiple file uploads?
});
// When the Media library Modal is opened...
mediaUploader.on('open', function() {
// if there is a file ID...
if(fileID) {
// select the file ID to show it as selected in the Media Library Modal.
mediaUploader.uploader.uploader.param( 'post_id', parseInt(fileID) );
var selection = mediaUploader.state().get('selection');
selection.add(wp.media.attachment(fileID));
}
});
// When a file is selected in the modal window....
mediaUploader.on('select', function() {
var attachment = mediaUploader.state().get('selection').first().toJSON(); // the selected file is added to the selection list
var fileDescription = $('textarea[name="FMDescription"]').val(); // get current File Description
var file = attachment.filename.split('.'); // get the filename and split into an array.
file.reverse(); // reverse the array to make it easier to get the file extension
$('.file-type').text('.'+file[0]); // File Extension
$('input[name="FMFile"]').val(attachment.url); // File URL
$('.file-header').text(attachment.title); // File Title
if(fileDescription == '') {
$('textarea[name="FMDescription"]').val(attachment.description);
} // Set File Description if nothing has been filled in yet...
$('.upload-date').text(attachment.dateFormatted); // File Upload Date Formatted (this will use WordPress's default setting)
$('.file-size').text(attachment.filesizeHumanReadable); // File Size
$('.file-options .edit').attr('href', attachment.editLink).text('Edit');
$('input[name="FM_attachment_id"]').val(attachment.id); // Attachment (file) ID
var date = $('#jquery-datepicker').val();
if(date) {
$('input[name="FMTitle"]').val(attachment.title+' '+date);
}
});
// open the Media Uploader.
mediaUploader.open();
});
Meta Box HTML
For reference this is the meta box HTML, it is also part of a PHP class.
I came across this odd bug. This only occurs if you try to upload a file before selecting a file. However if you select a file, then go back and try and upload a file it then works?
If I added the following to the mediaUploader it worked as it should, however this had the effect of losing the title option. I believe the title and button options only work on a custom frame.
This is maybe only needed in some edge cases, but you can use pointer-events to make psuedo elements (:after and :before) clickable via javascript.
Note: Preferable to use clickable elements if you can though!
My original code when I needed this as a work around.
SCSS Code
.add-chevron {
// prevents clicking on element from working
pointer-events: none;
// Necessary for allowing any clickable elements within the parent to work.
a, a:visited, button {
pointer-events: all;
}
&:after {
content: 'click me';
display: block;
height: 34px;
width: 34px;
// allows this pseudo element to be clickable
pointer-events: all;
}
}