When building tables in Lit, sometimes I need to generate different parts of a table in different parts of the code. But when I put everything together, the table does not look the same as if it were declared all in one place.
See this playground for an example of the same table that's assembled two different ways. Is there any way I can make the "two halves" table look the same as the first, while still creating the elements in separate html`` blocks?
I tried creating a table in two different ways (see playground), I expected it to be the same resulting table in both instances. What actually happened is that they looked different, and I want to know why/how to correct this.
Lit does not do string concatenation and each html tag function results in a cached Template element which can be efficiently rendered. This means that each of your html tags get implicitly closed by the browser's HTML parser.
E.g.: html`<table>...` is parsed by the browser into: html`<table>...</table>`. From lit.dev documentation on "Well-formed HTML": https://lit.dev/docs/templates/expressions/#well-formed-html
Lit templates must be well-formed HTML. The templates are parsed by the browser's built-in HTML parser before any values are interpolated. Follow these rules for well-formed templates:
Templates should not contain unclosed elements—they will be closed by the HTML parser.
Therefore instead of the following structure:
// Does not work correctly - do not copy this. For demonstration purposes.
const start = html`<table>`;
const content = html`content`;
const end = html`</table>`;
const result = [start, content, end];
return html`${result}`;
// This would render as: <table></table>content<table></table>
Consider the following structure instead where each html tag function is well formed HTML:
const tableFrame = (content) => html`<table>${content}</table>`;
const content = html`content`;
// Pass the content template result into the table frame.
return tableFrame(content);
The following is your code from the playground example restructured in this way:
<script type="module">
import {LitElement, html, css} from "https://cdn.jsdelivr.net/gh/lit/dist#2/core/lit-core.min.js";
class TableExample extends LitElement {
static styles = css`table, th, td { border: 1px solid black; }`;
generateTables() {
const tables = [];
const tableRow1 = html`
<tr>
<td rowspan=2>0</td>
<td>1</td>
</tr>`;
const tableRow2 = html`
</tr>
<td>2</td>
</tr>`;
const table1 = html`
<table>
<tr>
<td rowspan=2>0</td>
<td>1</td>
</tr>
</tr>
<td>2</td>
</tr>
</table>
`;
const tableFrame = (content) => html`<table>${content}</table>`;
// Full table
tables.push(table1);
tables.push(html`<br />`);
// Use tableFrame with custom content.
tables.push(tableFrame(html`<tr><td>Custom Content</td></tr>`));
tables.push(html`<br />`);
// Use tableFrame with the two rows defined earlier.
tables.push(tableFrame([tableRow1, tableRow2]));
return tables;
}
render() {
return this.generateTables();
}
}
customElements.define('table-example', TableExample)
</script>
<table-example></table-example>
As an additional reference the documentation on Templates: https://lit.dev/docs/templates/expressions/#templates
Accepting image uploads from a paste into the browser window is much easier than traditional file upload form inputs and even the newer style drag 'n' drop file uploads.
How do I implement it?
Here's an example PHP/JavaScript page that accepts drag 'n' drop image uploads. It's not dependent on PHP though - you could adapt it quite easily to work with another server-based language. This code was based on a snippet I found on jsFiddle by someone called Nick.
This is a full page - so you should be able to copy the code below and put it in a file on your web-server as-is (if you're not running PHP then you'll need to update the PHP code at the top or point the form to your own form handler script).
<?php
if (!empty($_POST)) {
// Handle the POSTed data here - the image is actually base64 encoded data in
// the $_POST['myTextarea'] variable which you can run through the base64_decode()
// function and then write to a file
$pos = strpos($_POST['myTextarea'], 'base64,');
$encoded = substr($_POST['myTextarea'], $pos + 7);
$raw = base64_decode($encoded);
// Show the base64 encoded $data - use the $raw variable when writing to a file
var_dump($_POST);
exit;
}
?>
<!DOCTYPE html >
<html>
<body>
<h1>File upload using paste</h1>
<p>
You can paste an image, which is on your clipboard, into this window and it will appear below.
If you use Windows you can press <b>PrtScr</b> to get a screenshot on your clipboard. Then
press <b>CTRL+V</b> to paste it into this document.
</p>
<!-- PUT THE ADDRESS OF YOUR FORM HANDLER SCRIPT IN THE ACTION ATTRIBUTE -->
<form action="" method="post">
<div id="form-elements-container">
<input type="text" value="An example text input..." name="myTextInput"><br />
<input type="submit" value="Submit form"><br />
</div>
</form>
<!-- THIS IS WHERE THE IMAGE THUMBNAILS WILL APPEAR -->
<div id="images-container"></div>
<script>
counter = 0;
document.body.onpaste = function (e)
{
// use event.originalEvent.clipboard for newer chrome versions
var items = (e.clipboardData || e.originalEvent.clipboardData).items;
// Find pasted image among pasted items
var blob = null;
for (var i=0; i<items.length; i++) {
if (items[i].type.indexOf("image") === 0) {
blob = items[i].getAsFile();
}
}
// Load image if there is a pasted image
if (blob !== null) {
var reader = new FileReader();
reader.onload = function(e)
{
// Create a new image object from the pasted data
var img = new Image();
img.src = e.target.result;
img.width = 128;
img.height = 128;
img.style.margin = '5px';
// Append the file to the document
document.getElementById('images-container').appendChild(img);
// Add a new textarea to the form
var textarea = document.createElement('textarea');
textarea.name = 'myTextarea_' + counter++;
textarea.value = img.src;
textarea.style.width = '200px';
textarea.style.height = '200px';
document.getElementById('form-elements-container').appendChild(textarea);
};
reader.readAsDataURL(blob);
}
}
</script>
</body>
</html>
As you've probably found, there appears to be no equivalent way to add the following Excel form and associated VBA code to Google Sheets or Scripts or Forms:
Is there some add-in that can be used to pop up this image and its controls? This has to be used many times in an accounting sheet to categorize expenditures at tax time.
It may not look exactly the same but I was able to construct a custom dialog in a short period of time to show how HTML service can be used to produce similar results.
First I construct an HTML template that contains the 2 combo boxes with multiple lines.
HTML_Test.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= include('CSS_Test'); ?>
</head>
<body>
<div id="left">
<label for="expenseCategory">Expense Category</label><br>
<select id="expenseCategory" size="10">
</select>
</div>
<div id="middle">
<label for="expenseSubCategory">Expense Sub Category</label><br>
<select id="expenseSubCategory" size="10">
</select>
</div>
<?!= include('JS_Test'); ?>
</body>
</html>
Then a CSS file to contain all my element formatting.
CSS_Test.html
<style>
#expenseCategory {
width: 90%;
}
#expenseSubCategory {
width: 90%;
}
#left {
width: 25%;
float: left;
}
#middle {
width: 50%;
float: left;
}
</style>
And a javascript file for client side javascript. I've simply hard coded some data to show how the select elements are filled in but this could just as easily be done using template scripting, or google.script.run
<script>
var expenses = [["A","1","2","3"],
["B","4","5"],
["C","6","7","8","9","10"]
];
function expenseCategoryOnClick() {
try {
let expenseCategory = document.getElementById('expenseSubCategory');
expenseCategory.options.length = 0;
expenses[this.selectedIndex].forEach( (expense,index) => {
if( index > 0 ) {
let option = document.createElement("option");
let text = document.createTextNode(expense);
option.appendChild(text);
expenseCategory.appendChild(option);
}
}
);
}
catch(err) {
alert("Error in expenseCategoryOnClick: "+err)
}
}
(function () {
// load first expense
let expenseCategory = document.getElementById('expenseCategory');
expenseCategory.addEventListener("click",expenseCategoryOnClick);
expenses.forEach( expense => {
let option = document.createElement("option");
let text = document.createTextNode(expense[0]);
option.appendChild(text);
expenseCategory.appendChild(option);
}
);
expenseCategory = document.getElementById('expenseSubCategory');
expenses[0].forEach( (expense,index) => {
if( index > 0 ) {
let option = document.createElement("option");
let text = document.createTextNode(expense);
option.appendChild(text);
expenseCategory.appendChild(option);
}
}
);
}
)();
</script>
Then there is the server side code bound to a spreadsheet.
Code.gs
function onOpen(e) {
var menu = SpreadsheetApp.getUi().createMenu("Test");
menu.addItem("Show Test","showTest");
menu.addToUi();
}
// include(filename) required to include html files in the template
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename)
.getContent();
}
function showTest() {
var html = HtmlService.createTemplateFromFile("HTML_Test");
html = html.evaluate();
html.setWidth(800);
SpreadsheetApp.getUi().showModalDialog(html,"Test");
}
The dialog looks like this. Many more html elements can be added as needed. This just shows the basics. This may be more difficult than an wysiwig html editor but I find I have better control of the appearance and function of my pages this way. Notice I clicked "C" and the sub category is filled in automatically.
I have developed a Laravel web application with the help of some tutorial videos and codes given by other developers on stackoverflow .The app is working pretty good except for the image upload feature. I am facing an issue related to the uploaded image being cut either on the sides or on the bottom as well as the image when uploaded through any IOS device then the image under goes rotation. I have installed image intervention but i don't know where to put the code inside my files i am sharing my controller code as well as the image displaying code here
controller code
namespace App\Http\Controllers;
use Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\MessageBag;
use App\comment;
use App\User;
use App\post;
use View;
use Lang;
use image;
class usersController extends Controller
{
private $u;
public function __construct(){
$this->u = new User();
}
public function search(Request $request){
$search_input = $request["input"];
$path = $request["path"];
$users = User::where("name","like","%$search_input%")->orWhere("username","like","%$search_input%")->get();
if ($users->isEmpty()) {
return "<p style='text-align: center;width: 100%;color: gray;font-size: 12px;margin: 3px'>".Lang::get('trans.nothingToShow')."</p>";
}else{
foreach ($users as $user) {
if ($user->verify == 1) {
$ifVerify = '<span class="verify-badge" style="width: 420px; height: 700px; background: url(\''.$path.'/imgs/verify.png\') no-repeat; background-size: cover; margin: 0px 2px;"></span>';
}else{
$ifVerify = '';
}
if($user->avatar == "avatar.png"){
$avatar_path = '/imgs/avatar.png';
}else{
$avatar_path = '/storage/avatar/'.$user->avatar;
}
echo '<div class="navbar-search-item">
<a href="'.$path.'/'.$user->username.'">
<div>
<div style="background-image:url(\''.$path.$avatar_path.'\');">
</div>
<p>
'.$user->name.$ifVerify.'<br>
<span>#'.$user->username.'</span>
</p>
</div>
</a>
</div>';
}
}
}
public function profile($username){
$user_info = User::where("username",$username)->get();
foreach ($user_info as $uinfo) {
$user_id = $uinfo->uid;
}
if (isset($user_id)) {
$feedbacks = post::where("to_id",$user_id)->where("privacy",1)->orderBy('time', 'desc')->get();
$feedbacks_count = post::where("to_id",$user_id)->get()->count();
$new_count = post::where("to_id",$user_id)->where('read',0)->get()->count();
// check comments on post (count)
$commentsCount = array();
foreach ($feedbacks as $fb) {
$pid = $fb->pid;
$countComments = comment::where("c_pid",$pid)->get()->count();
array_push($commentsCount,$countComments);
}
return view("pages.profile")->with(["user_info" => $user_info,"feedbacks" => $feedbacks,'feedbacks_count' => $feedbacks_count,'new_count' => $new_count,'commentsCount' => $commentsCount]);
}else{
return view("pages.profile")->with(["user_info" => $user_info]);
}
}
public function settings($username){
$user_info = User::where("username",$username)->get();
if (Auth::user()->username == $username) {
return view("pages.settings")->with("user_info",$user_info);
}else{
return redirect()->back();
}
}
public function s_general(Request $request){
$this->validate($request,[
'avatar' => 'nullable|image|mimes:jpeg,png,jpg|max:3072',
'fullname' => 'required',
'email' => 'required|email'
]);
if ($request['fullname'] == Auth::user()->name && $request['email'] == Auth::user()->email && !$request->hasFile('avatar')) {
return redirect()->back()->with('general_msg', Lang::get('trans.noChanges_MSG'));
}else{
$avatar = $request->file('avatar');
if ($request->hasFile('avatar')) {
$avatar_ext = $avatar->getClientOriginalExtension();
$avatar_name = rand(9,999999999)+time().".".$avatar_ext;
$avatar_new = $avatar->storeAs("avatar",$avatar_name);
}else{
$avatar_name = Auth::user()->avatar;
}
$update_general = User::where('uid',Auth::user()->uid)->update(['name' => $request['fullname'],'email' => $request['email'],'avatar' => $avatar_name]);
return redirect()->back()->with('general_msg', Lang::get('trans.changes_saved'));
}
}
and this is how i display the image
<div class="profile-avatar" style="width: 300px;height:400px; border-radius: 0%;background-image: url('#if(Auth::user()->avatar == "avatar.png") {{ url("/imgs/".Auth::user()->avatar) }} #else {{ url("/storage/avatar/".Auth::user()->avatar) }} #endif');">
please help me with code should i put and where i should put that in order to resolve this issue
this code is for preview of the image
<div style="display: inline-flex;">
<div class="profile-avatar" id="settings_img_elm" style="margin: 10px; width:350px;margin-top: 0; margin-bottom: 0;border-color: #fff; text-align: center;background-image: url('#if(Auth::user()->avatar == "avatar.png") {{ url("/imgs/".Auth::user()->avatar) }} #else {{ url("/storage/avatar/".Auth::user()->avatar) }} #endif');">
</div>
<p style="color: #a7aab5; font-size: 9px;padding: 25px 10px 25px 10px; margin: 0;">#lang("trans.preview")<br>#lang("trans.maxSize")<br>upload vertical <br>images.<br>Save the<br>image first<br> and then<br> check the<br> preview</p>
</div>
<p style="border-bottom: 1px solid #dfe2e6;margin: 0; margin-top: 12px; margin-bottom: 12px;">
<input type="file" name="avatar" style="display: none;" id="settings_img">
<label for="settings_img" class="btn btn-success">#lang("trans.selectImage")</label>
the javascript for the preview image is
function imagePreview(input,elm) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
$(elm).css("background-image","url('"+e.target.result+"')");
}
reader.readAsDataURL(input.files[0]);
}
}
$("#settings_img").on("change",function(){
imagePreview(this,"#settings_img_elm");
});
$("#feedback_hFile").on("change",function(){
$(".send_feedback_image").show();
imagePreview(this,"#sfb_image_preview");
});
The code below should get you started. The two key things you are looking for here are Interventions orientate and resize methods which should take care of the two issues you mention. Orientation will rotate the image based on EXIF data, and resize can resize your image to whatever specifications you need.
Import the facade
use Intervention\Image\Facades\Image;
Suggestions
Remove use image; from your imports as it is probably causing or going to cause you issues. It is invalid.
s_general method adjustments
public function s_general(Request $request){
$this->validate($request,[
'avatar' => 'nullable|image|mimes:jpeg,png,jpg|max:3072',
'fullname' => 'required',
'email' => 'required|email'
]);
if ($request['fullname'] === Auth::user()->name && $request['email'] === Auth::user()->email && !$request->hasFile('avatar')) {
return redirect()->back()->with('general_msg', Lang::get('trans.noChanges_MSG'));
}
if ($request->hasFile('avatar')) {
// Grab the original image from the request
$originalImage = $request->file('avatar');
// Grab the original image extension
$originalExtension = $originalImage->getClientOriginalExtension();
// Instantiate a new Intervention Image using our original image,
// read the EXIF 'Orientation' data of the image and rotate the image if needed
$newImage = Image::make($originalImage)->orientate();
// Resize the new image to a width of 300 and a height of null (auto)
// we also use a callback to constrain the aspect ratio so we don't distort the image on resize
$newImage->resize(300, null, function ($constraint) {
$constraint->aspectRatio();
});
// Generate a randomized filename and append the original image extension
$filename = random_int(9, 999999999) + time() . '.' . $originalExtension;
// Save the new image to disk
$newImage->save(storage_path('app/public/avatar/' . $filename));
} else {
$filename = Auth::user()->avatar;
}
User::where('uid', Auth::user()->uid)->update(
[
'name' => $request['fullname'],
'email' => $request['email'],
'avatar' => $filename
]);
return redirect()->back()->with('general_msg', Lang::get('trans.changes_saved'));
}
More suggestions
I know this isn't a code review, but
Use PascalCase for your class names. I see you have a few imports such as App\comment and App\post
Your constructor doesn't seem to be needed. I'd ditch it. If you are keeping it, i would get use to more descriptive variable names. Short names like $u are generally considered bad practice.
You have a few unused imports, Validator, Hash and MessageBag could be removed to clean this up.
Your controller is doing a lot of stuff that most would consider bad practice. Fumbling around with html for example. 9.9 times out of 10 you should probably be leveraging blade for these things as it's one of its main purposes.
Stick to one naming convention or another. You are using a mixture of camelCase and snake_case for your variables. I prefer camelCase but whichever you choose it's best to stick with it and not mix them.
Sorry, i know this isn't suppose to be a code review, i just thought that a few little suggestions might help you in the future.
You can check Intervation docs on how to resize image, I've also created an example for you for s_general function:
$avatar = $request->file('avatar');
if ($request->hasFile('avatar')) {
//pass uploaded avatar to Intervention Image instance
$image = \Image::make($avatar);
//resize image to 300 width, keep height automated adjusted, or any other width you need
$image->resize(300, null, function ($constraint) {
$constraint->aspectRatio();
});
//Or you can set specific width and height
//$image->resize(300, 200);
$avatar_ext = $avatar->getClientOriginalExtension();
$avatar_name = rand(9,999999999) + time().".".$avatar_ext;
//I used public path to store the image you can change it based on your needs
$image->save(public_path('/avatar/'.$avatar_name));
}else{
$avatar_name = Auth::user()->avatar;
}
use image intervention for resizing and for correcting the orientation of the image uploaded from a mobile phone. IF you are showing the preview of this image using simple javascript something like
reader.onload = function (e) {
$(elm).css("background-image","url('"+e.target.result+"')");
then there is a chance that the preview of the image won't be oriented as we want but once you save the image the orientation will be proper.
this problem of orientation is encountered mostly on iphone.
The code for orientation correction and resizing is
$newImage = Image::make($originalImage)->orientate();
$newImage->resize(300, null, function ($constraint) {
$constraint->aspectRatio();
});
///or for resizing use
$newImage->resize(width,height);
I have custom editor toolbar plugin which inserts html tags. The tag opens with div. Every time when page is saved or published the editor adds a new p tag above it.
here is the image
IMG tag drifts after publish I see a new p tag is inserted.
The Img being replaced by an html before saving
<p>
<div class="ssimage_code">
<img src="src_path0" alt=""/ >
<img src="src_path1" alt="" / >
</div>
</p>
I want to see if it gets solved by replacing a p tag instead of a img tag.
How do I access the parent tag from Node?
getParent does not work
Code
ed.serializer.addNodeFilter('img', function(nodes, name, args) {
var i = nodes.length, node;
while (i--) {
node = nodes[i];
if ((node.attr('class') || '').indexOf('slider_toimg') !== -1) {
self.imgToslidercode(node, args);
}
}
});
imgToslidercode:function(node,args){
insertcode = '';
node.replace(insertcode);
}
What I am looking here is?
node.getParent().replace(insertcode);