Dynamically update syntax highlighting for the Ace Editor + Requirejs - javascript

I need to dynamically change the set of keywords that need to highlight. Here was the answer to a similar theme, but my project already has require.js and when I use the code from the response then I have an error:
Module name "DynHighlightRules" has not been loaded yet for context: _
Then I use files from ace-builds and try get ace using requirejs. This is my code:
<!DOCTYPE html>
<html lang="en">
<head>
<title>ACE in Action</title>
<style type="text/css" media="screen">
#e1 {
position: absolute;
top: 0;
right: 0;
bottom: 50%;
left: 0;
}
</style>
</head>
<body>
<div id="e1">
function foo(items) {
var x = "All this is syntax highlighted";
return x;
}
first second editor
</div>
<script src="require.js"></script>
<script>
require.config({
baseUrl: window.location.protocol + "//" + window.location.host
+ window.location.pathname.split("/").slice(0, -1).join("/"),
paths: {
ace: "/home/sergey/ace-builds-master/src/",
}
});
define("DynHighlightRules", function() {
this.setKeywords = function(kwMap) {
this.keywordRule.onMatch = this.createKeywordMapper(kwMap, "identifier")
}
this.keywordRule = {
regex : "\\w+",
onMatch : function() {return "text"}
}
this.$rules = {
"start" : [
{
token: "string",
start: '"',
end: '"',
next: [{ token : "constant.language.escape.lsl", regex : /\\[tn"\\]/}]
},
this.keywordRule
]
};
});
require(["ace/ace"], function (ace) {
var editor = ace.edit("e1");
var TextMode = require("ace/mode/text").Mode;
var dynamicMode = new TextMode();
dynamicMode.HighlightRules = require(["DynHighlightRules"]);
editor.session.setMode(dynamicMode);
var tags = ["first", "second"];
dynamicMode.$highlightRules.setKeywords({"keyword": tags.join("|")})
editor.session.bgTokenizer.start(0)
});
</script>
</body>
</html>
This code not working. How can I add new mode to the ace if I already have requirejs in my project?
Thank you!

If you have require.js the best solution is to put DynHighlightRules into it's own file, but if you prefer keeping it inline, you can do the following
define("DynHighlightRules", function(require, exports, module) {
var oop = require("ace/lib/oop");
var TextHighlightRules = require("ace/mode/text_highlight_rules")
.TextHighlightRules;
module.exports = function() {
this.setKeywords = function(kwMap) {
this.keywordRule.onMatch = this.createKeywordMapper(kwMap, "identifier")
}
this.keywordRule = {
regex: "\\w+",
onMatch: function() {
debugger;
return "text"
}
}
this.$rules = {
"start": [{
token: "string",
start: '"',
end: '"',
next: [{
token: "constant.language.escape.lsl",
regex: /\\[tn"\\]/
}]
},
this.keywordRule
]
};
this.normalizeRules()
}
module.exports.prototype = TextHighlightRules.prototype
});
require(["ace/ace", "DynHighlightRules"], function(ace) {
var editor = ace.edit("e1");
var TextMode = require("ace/mode/text").Mode;
var dynamicMode = new TextMode();
dynamicMode.$id = "DynHighlightRules";
dynamicMode.HighlightRules = require("DynHighlightRules");
editor.session.setMode(dynamicMode);
var tags = ["first", "second"];
dynamicMode.$highlightRules.setKeywords({"keyword": tags.join("|")})
editor.session.bgTokenizer.start(0)
});
note that you should call require("DynHighlightRules"); instead of require(["DynHighlightRules"]); since the later form doesn't return a module/
also DynHighlightRules needs to be in dependency list of main require, to trigger require.js to process queue of pending defines, and you need to set proper prototype and call normalizeRules on text mode enable using rules with start/end

Related

Why does creating a color command in an HTML Terminal emulator create a (mostly) white screen?

I am making an HTML terminal, and have done most of the styling. I am trying to add one command, though, which will change the color. You can run these code snippets, and see why I'm asking this, but when you type in color and then a color, instead of doing what I'm trying to make it do, it just gives you a (mostly) white screen. Please help, as this needs to be finished before tommorow.
$('body').terminal({
iam: function(name) {
this.echo('Hello, ' + name +
'. Welcome to Coding Class!');
},
echo: function(what) {
this.echo(what)
},
link: function(link) {
window.location.replace("http://www." + link);
},
color: function(color) {
var body = document.querySelector("body");
body.className = "test";
var temp = document.querySelectorAll(".test");
for (var i = 0; i < temp.length; i++) {
temp[i].style.color = color;
temp[i].style.fontSize = "40px";
}
}
}, {
greetings: 'Hi!',
prompt: 'root> '
})
<head>
<script src="https://code.jquery.com/jquery-3.3.1.min.js">
</script>
<script src="https://unpkg.com/jquery.terminal/js/jquery.terminal.min.js">
</script>
<link rel="stylesheet" href="https://unpkg.com/jquery.terminal/css/jquery.terminal.min.css" />
</head>
<body>
</body>
let $ = jQuery.noConflict();
$('body').terminal({
iam: function(name) {
this.echo('Hello, ' + name +
'. Welcome to Coding Class!');
},
echo: function(what) {
this.echo(what)
},
link: function(link) {
window.location.replace("http://www." + link);
},
color: function(color) {
var body = document.querySelector("body");
body.classList.add('test');// add class name `test` to body.
var temp = document.querySelectorAll(".test");
for (var i = 0; i < temp.length; i++) {
temp[i].style.setProperty('--background', color);// use set variable instead of `background-color`.
//temp[i].style.backgroundColor = color;// not recommended
temp[i].style.fontSize = "40px";
}
}
}, {
greetings: 'Hi!',
prompt: 'root> '
})
<head>
<script src="https://code.jquery.com/jquery-3.3.1.min.js">
</script>
<script src="https://unpkg.com/jquery.terminal/js/jquery.terminal.min.js">
</script>
<link rel="stylesheet" href="https://unpkg.com/jquery.terminal/css/jquery.terminal.min.css" />
</head>
<body>
</body>
I'm showing you how to use add() CSS class name to body.
To make background color change on command color, I change the code to use setProperty() to set CSS variable instead because the CSS file itself is already use that variable (in .terminal class) and it is easier this way.

where to reference external js file in html button

I'm doing an exercise from a course in Udacity and they give us an html with its functions in javascript. The webpage consists of a form in which you write something and when you submit it then that is added to the page in a blue square. The point of this is to show us how the mvo design pattern works, so the js file is divided in the model, the view and the octopus which connects the two previous. Just to play around, I wanted to add a "remove" button that removed the last block in the page. I kind of coppied the function that added a new block, but used .pop() instead of .push() in order to manipulate the localstorage of the page. I think the function is correct, but I can't figure out how to "call" the function. I've tried to add an event listener to the button. I also tried to use .submit() from jquery with event.preventDefault(); to put the remove function as a parameter of .submit(). I think the closest option would be to reference the .js with tags and then call the function inside the buttons onclick attribute, but it's not working (the function is a method of an object that is inside the .js, so I tried calling it like this <button onclick = "javascript:octopus.remove()">remove!</button>). Also tried using onclick but on javascript document.getElementById("button").onclick = function(){}; but nothing. Any help? this is the js
$(function(){
var model = {
init: function() {
if (!localStorage.notes) {
localStorage.notes = JSON.stringify([]);
}
},
add: function(obj) {
var data = JSON.parse(localStorage.notes);
data.push(obj);
localStorage.notes = JSON.stringify(data);
},
getAllNotes: function() {
return JSON.parse(localStorage.notes);
},
//here I tried everything but nothing seems to work
remove: function() {
document.getElementById("button").onclick =
function(){
var data = JSON.parse(localStorage.notes);
data.push(obj);
localStorage.notes = JSON.stringify(data);
};
}
};
var octopus = {
addNewNote: function(noteStr) {
model.add({
content: noteStr
});
view.render();
},
getNotes: function() {
return model.getAllNotes().reverse();
},
init: function() {
model.init();
view.init();
},
removeNote: function(){
model.remove();
view.render();
}
};
var view = {
init: function() {
this.noteList = $('#notes');
var newNoteForm = $('#new-note-form');
var newNoteContent = $('#new-note-content');
newNoteForm.submit(function(e){
octopus.addNewNote(newNoteContent.val());
newNoteContent.val('');
e.preventDefault();
});
view.render();
},
render: function(){
var htmlStr = '';
octopus.getNotes().forEach(function(note){
htmlStr += '<li class="note">'+
note.content +
'</li>';
});
this.noteList.html( htmlStr );
},
};
octopus.init();
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Udacity Retain</title>
<link rel="stylesheet" href="css/retain.css">
</head>
<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="js/retain.js"></script>
<form id="new-note-form" class="new-note-form">
<input id="new-note-content" class="new-note-content">
</form>
<button id = "button">remove!</button>
<ul id="notes" class="notes"></ul>
</body>
</html>
You are nearly there.. I've updated your code to work, see below.
Firstly, you need to make model.remove() actually delete an item by using pop() and then saving the updated data.
Secondly, you need to hook up a click event to the remove button. I've added this in view.init() after where you hook up the form submit.
$(function(){
var model = {
init: function() {
if (!localStorage.notes) {
localStorage.notes = JSON.stringify([]);
}
},
add: function(obj) {
var data = JSON.parse(localStorage.notes);
data.push(obj);
localStorage.notes = JSON.stringify(data);
},
getAllNotes: function() {
return JSON.parse(localStorage.notes);
},
// Updated method below
remove: function() {
var data = JSON.parse(localStorage.notes);
data.pop();
localStorage.notes = JSON.stringify(data);
}
};
var octopus = {
addNewNote: function(noteStr) {
model.add({
content: noteStr
});
view.render();
},
getNotes: function() {
return model.getAllNotes().reverse();
},
init: function() {
model.init();
view.init();
},
removeNote: function(){
model.remove();
view.render();
}
};
var view = {
init: function() {
this.noteList = $('#notes');
var newNoteForm = $('#new-note-form');
var newNoteContent = $('#new-note-content');
var removeBtn = $('#button');
newNoteForm.submit(function(e){
octopus.addNewNote(newNoteContent.val());
newNoteContent.val('');
e.preventDefault();
});
// Added click event on the remove button
removeBtn.on('click', function() {
octopus.removeNote();
});
view.render();
},
render: function(){
var htmlStr = '';
octopus.getNotes().forEach(function(note){
htmlStr += '<li class="note">'+
note.content +
'</li>';
});
this.noteList.html( htmlStr );
},
};
octopus.init();
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Udacity Retain</title>
<link rel="stylesheet" href="css/retain.css">
</head>
<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="js/retain.js"></script>
<form id="new-note-form" class="new-note-form">
<input id="new-note-content" class="new-note-content">
</form>
<button id = "button">remove!</button>
<ul id="notes" class="notes"></ul>
</body>
</html>

Combine HTML and JS from Fiddle

A similar question was asked in this forum post, but I am still unable to convert the code from JSfiddle to HTML.
The JSfiddle example can be found here.
I tried to use the technique suggested in the forum post previously mentioned, that is:
<html>
<head>
<style type="text/css">
// CSS Content
</style>
</head>
<body ng-app="myApp">
<!-- some html elements -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<script language="JavaScript" type="text/javascript">
// more js here.
</script>
</body>
As I am a complete noob, I simply copied the HTML and Javascript into the some html elements and // more js here sections. Without changing the API key, the final code looked like this:
<html>
<head>
<style type="text/css">
// CSS Content
</style>
</head>
<body ng-app="myApp">
<div ng-app="myapp" ng-controller="WeatherCtrl">
<h2>Weather in Salzburg, Austria</h2>
<weather-icon cloudiness="{{ weather.clouds }}"></weather-icon>
<h3>Current: {{ weather.temp.current | temp:2 }}</h3>
min: {{ weather.temp.min | temp }}, max: {{ weather.temp.max | temp }}
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.min.js"></script>
<script language="JavaScript" type="text/javascript">
'use strict';
var myapp = angular.module('myapp', []);
myapp.factory('weatherService', function($http) {
return {
getWeather: function() {
var weather = { temp: {}, clouds: null };
$http.jsonp('http://api.openweathermap.org/data/2.5/weather?q=Salzburg,at&units=metric&callback=JSON_CALLBACK&APPID=f9dbd911bc01df1d9ce563b2ba4d3209').success(function(data) {
if (data) {
if (data.main) {
weather.temp.current = data.main.temp;
weather.temp.min = data.main.temp_min;
weather.temp.max = data.main.temp_max;
}
weather.clouds = data.clouds ? data.clouds.all : undefined;
}
});
return weather;
}
};
});
myapp.filter('temp', function($filter) {
return function(input, precision) {
if (!precision) {
precision = 1;
}
var numberFilter = $filter('number');
return numberFilter(input, precision) + '\u00B0C';
};
});
myapp.controller('WeatherCtrl', function ($scope, weatherService) {
$scope.weather = weatherService.getWeather();
});
myapp.directive('weatherIcon', function() {
return {
restrict: 'E', replace: true,
scope: {
cloudiness: '#'
},
controller: function($scope) {
$scope.imgurl = function() {
var baseUrl = 'https://ssl.gstatic.com/onebox/weather/128/';
if ($scope.cloudiness < 20) {
return baseUrl + 'sunny.png';
} else if ($scope.cloudiness < 90) {
return baseUrl + 'partly_cloudy.png';
} else {
return baseUrl + 'cloudy.png';
}
};
},
template: '<div style="float:left"><img ng-src="{{ imgurl() }}"></div>'
};
});
</script>
</body>
My output looks like this:
Could anyone please show me how to do this?
The problem is that you are declaring two apps remove "ng-app="myApp" from the body <body> tag.

Internationalization of HTML pages for my Google Chrome Extension

I found a very easy way to implement translation (or localization) of my Google Chrome Extension, but that seems to apply only to .json, css and js files.
But how to localize my html content, say in the popup or an options window?
What you would do is this.
First, in your HTML use the same syntax as Chrome requires anywhere else. So your basic popup.html will be:
<!DOCTYPE html>
<html>
<head>
<title>__MSG_app_title__</title>
</head>
<body>
__MSG_link001__
<!-- Need to call our JS to do the localization -->
<script src="popup.js"></script>
</body>
</html>
Then provide the usual translation in _locales\en\messages.json:
{
"app_title": {
"message": "MyApp",
"description": "Name of the extension"
},
"link001": {
"message": "My link",
"description": "Link name for the page"
},
"prompt001": {
"message": "Click this link",
"description": "User prompt for the link"
}
}
And finally your popup.js will perform the actual localization:
function localizeHtmlPage()
{
//Localize by replacing __MSG_***__ meta tags
var objects = document.getElementsByTagName('html');
for (var j = 0; j < objects.length; j++)
{
var obj = objects[j];
var valStrH = obj.innerHTML.toString();
var valNewH = valStrH.replace(/__MSG_(\w+)__/g, function(match, v1)
{
return v1 ? chrome.i18n.getMessage(v1) : "";
});
if(valNewH != valStrH)
{
obj.innerHTML = valNewH;
}
}
}
localizeHtmlPage();
Plain an simple:
{
"exmaple_key": {
"message": "example_translation"
}
}
<sometag data-locale="example_key">fallback text</sometag>
document.querySelectorAll('[data-locale]').forEach(elem => {
elem.innerText = chrome.i18n.getMessage(elem.dataset.locale)
})
Building from ahmd0's answer. Use a data attribute to allow a hard-coded fallback.
<!DOCTYPE html>
<html>
<head>
<title data-localize="__MSG_app_title__">My Default Title</title>
</head>
<body>
Default link text
<script src="localize.js"></script>
</body>
</html>
Then provide the usual translation in _locales\en\messages.json:
{
"app_title": {
"message": "MyApp",
"description": "Name of the extension"
},
"link001": {
"message": "My link",
"description": "Link name for the page"
},
"prompt001": {
"message": "Click this link",
"description": "User prompt for the link"
}
}
And finally your localize.js will perform the actual localization:
function replace_i18n(obj, tag) {
var msg = tag.replace(/__MSG_(\w+)__/g, function(match, v1) {
return v1 ? chrome.i18n.getMessage(v1) : '';
});
if(msg != tag) obj.innerHTML = msg;
}
function localizeHtmlPage() {
// Localize using __MSG_***__ data tags
var data = document.querySelectorAll('[data-localize]');
for (var i in data) if (data.hasOwnProperty(i)) {
var obj = data[i];
var tag = obj.getAttribute('data-localize').toString();
replace_i18n(obj, tag);
}
// Localize everything else by replacing all __MSG_***__ tags
var page = document.getElementsByTagName('html');
for (var j = 0; j < page.length; j++) {
var obj = page[j];
var tag = obj.innerHTML.toString();
replace_i18n(obj, tag);
}
}
localizeHtmlPage();
The hard-coded fallback avoids the i18n tags being visible while the JavaScript does the replacements. Hard-coding seems to negate the idea of internationalisation, but until Chrome supports i18n use directly in HTML we need to use JavaScript.
As RobW noted in a comment, a feature request for adding i18n support in HTML using the same mechanism was created, but it has since then been rejected due to performance and security concerns. Therefore you can't use the same approach.
The issue mentions one possible workaround: to have separate HTML pages per language and switch between them in the manifest:
"browser_action": {
"default_popup": "__MSG_browser_action_page__"
}
But if that's not a suitable approach, the only way is to translate the page dynamically via JavaScript. You mention a solution the simplest approach, by just tagging elements to translate with ids and replacing them on page load.
You can also employ more sophisticated tools like webL10n in parallel with Chrome's approach. Note that you should probably still minimally implement Chrome's approach, so that Web Store knows that the item is supporting several languages.
Rather than parsing the full DOM, just add a class "localize" to the elements that have to be translated and add a data attribute data-localize="open_dashboard"
<div class="localize" data-localize="open_dashboard" >
Open Dashboard
</div>
JavaScript :
$('.localize').each(function(index,item){
var localizeKey = $(item).data( 'localize' );
$(item).html(chrome.i18n.getMessage(localizeKey));
});
'_locales/en/messages.json' file
{
"open_dashboard": {
"message": "Open Dashboard",
"description": "Opens the app dashboard"
}
}
A workaround to avoid replacements:
Use a simple "redirect"
It works for popups and options
In your manifest, declare the default popup
"default_popup": "popup/redirect.html"
The popup/redirect.html is almost empty. It just includes the script link to the redirect script
<!DOCTYPE html>
<html>
<head></head>
<body>
<script src="redirect.js"></script>
</body>
The popup/redirect.js file is very simple too:
var currentlang = chrome.i18n.getMessage("lang");
var popupUrl = chrome.runtime.getURL("popup/popup-"+currentlang+".html");
window.location.href = popupUrl;
Create multiple popups, already localized:
popup-fr.html
popup-en.html
Go into each of your messages.json files (in _locales) and add a "lang" message with the current language abbreviation as value: en for the english json, fr in the french json...
example for _locales/en/message.json:
"lang": {
"message": "en",
"description": "Locale language of the extension."
},
A simple workaround for very small project... definitely not a good choice for large ones. And it also works for Option pages.
One of the ways to localize your content in popup html is to fetch it from javascript onLoad. Store the strings in the _locales folder under various languages supported by you as mentioned here and do chrome.i18n.getMessage("messagename") to fetch and load the variable strings and set them using javascript/jquery onLoad function for each html element from your background.js or whatever js you load before your html pages loads.
I faced the same problem, but I solved it with a simple approach using custom data attributes.
Implement a localizing class that uses chrome.i18n and call it in the DOMContentLoaded event. In HTML, mark up the element you want to localize with the data-chrome-i18n attribute. (This attribute name is tentatively named.) Specifying the message name as the value of this attribute localizes the text content of the element. If you want to localize an attribute, specify it in the format attribute_name=message_name. Multiple specifications can be specified by separating them with ;.
const i18n = (window.browser || window.chrome || {}).i18n || { getMessage: () => undefined };
class Localizer {
constructor(options = {}) {
const { translate = Localizer.defaultTranslate, attributeName = Localizer.defaultAttributeName, parse = Localizer.defaultParse } = options;
this.translate = translate;
this.attributeName = attributeName;
this.parse = parse;
}
localizeElement(element) {
for (const [destination, name] of this.parse(element.getAttribute(this.attributeName))) {
if (!name)
continue;
const message = this.translate(name) || '';
if (!destination) {
element.textContent = message;
}
else {
element.setAttribute(destination, message);
}
}
}
localize(target = window.document) {
const nodes = target instanceof NodeList ? target : target.querySelectorAll(`[${CSS.escape(this.attributeName)}]`);
for (const node of nodes)
this.localizeElement(node);
}
}
Localizer.defaultTranslate = i18n.getMessage;
Localizer.defaultAttributeName = 'data-chrome-i18n';
Localizer.defaultParse = (value) => {
return (value || '').split(';').map(text => (text.includes('=') ? text.split('=') : ['', text]));
};
const localizer = new Localizer();
window.addEventListener('DOMContentLoaded', () => {
localizer.localize();
});
<!DOCTYPE html>
<html data-chrome-i18n="lang=##ui_locale">
<head>
<meta charset="UTF-8" />
<title data-chrome-i18n="extensionName"></title>
</head>
<body>
<p data-chrome-i18n="foo;title=bar;lang=##ui_locale"></p>
</body>
</html>
There are several things to consider to solve this problem.
Use chrome.i18n (Many people will want to aggregate in messages.json.)
Supports attributes as well as element content
Supports not only popup but also options page
Rendering performance
Security
First, the approach of switching HTML for each language in manifest.json does not work. Even if you give __MSG_*__ to the default_popup field, popup will still show the error "ERR_FILE_NOT_FOUND". I don't know why. There is no detailed reference to default_popup in the Chrome extensions Developer Guide, but MDN mentions that it is a localizable property. Similarly, if you give __MSG _*__ to the page field in options_ui, the extension itself will fail to load.
I intuitively felt that the approach of replacing __MSG_*__ in HTML and rewriting the result usinginnerHTML had performance and security problems.
This answer is cool!
And I want to make some modifications.
For chrome 93.0.4577.63 chrome.i18n.getMessage permalink, link-by-version
chrome.i18n.getMessage(messageName, substitutions, {escapeLt})
So I want to make it support
substitutions
escapeLt
Test Data
// _locales/en/messages.json
{
"hello": {
"message": "<b>Hello</b> $USER$ Welcoming $OUR_SITE$. $EMOJI$",
"description": "Greet the user",
"placeholders": {
"user": {
"content": "$1", // chrome.i18n.getMessage("hello", "variable 1")
"example": "Carson"
},
"our_site": {
"content": "Example.com"
},
"emoji": {
"content": "$2",
"example": "\uD83D\uDE42" // 🙂, 😎
}
}
},
"app": {
"message": "My cool APP.",
"description": "description"
}
}
<!-- test.html-->
<script src="my-i18n.js"></script>
<p data-i18n="__MSG_hello__"></p>
<p data-i18n="__MSG_hello__<b>Carson</b>"></p>
<p data-i18n="__MSG_hello__<b>Carson</b>|0"></p>
<p data-i18n="__MSG_hello__<i>Carson</i>|1"></p>
<button title="__MSG_hello__<b>Carson</b>" data-i18n></button>
<button title="__MSG_hello__<b>Carson</b>|0" data-i18n></button>
<button title="__MSG_hello__<b>Carson</b>|1" data-i18n></button>
<p title="__MSG_app__" data-i18n="__MSG_hello__Carson,🙂"></p>
output
Script
// my-i18n.js
/**
* #param {string} msg "__MSG_Hello__para1,para2|1" or "__MSG_Hello__para1,para2|0"
* */
function convertMsgAsFuncPara(msg) {
const match = /__MSG_(?<id>\w+)__(?<para>[^|]*)?(\|(?<escapeLt>[01]{1}))?/g.exec(msg) // https://regex101.com/r/OeXezc/1/
if (match) {
let {groups: {id, para, escapeLt}} = match
para = para ?? ""
escapeLt = escapeLt ?? false
return [id, para.split(","), Boolean(Number(escapeLt))]
}
return [undefined]
}
function InitI18nNode() {
const msgNodeArray = document.querySelectorAll(`[data-i18n]`)
msgNodeArray.forEach(msgNode => {
const [id, paraArray, escapeLt] = convertMsgAsFuncPara(msgNode.getAttribute("data-i18n"))
if (id) {
msgNode.innerHTML = chrome.i18n.getMessage(id, paraArray, {escapeLt})
}
// ↓ handle attr
for (const attr of msgNode.attributes) {
const [attrName, attrValue] = [attr.nodeName, attr.nodeValue]
const [id, paraArray, escapeLt] = convertMsgAsFuncPara(attrValue)
if (!id) {
continue
}
msgNode.setAttribute(attrName, chrome.i18n.getMessage(id, paraArray, {escapeLt}))
}
})
}
(() => {
window.addEventListener("load", InitI18nNode, {once: true})
})()
Modify pseudo-category content in batches.
<div data-content="font"></div>
div::before {
content: attr(data-content);
}
document.querySelectorAll('[data-content]').forEach(el => {
el.dataset.content = chrome.i18n.getMessage(el.dataset.content);
});
Use CSS Internationalization.
<p></p>
p::before {
content: "__MSG_font__";
}
Another workaround - you can use content property in css with __MSG_myText inside.
Use Vue.js:
<html>
<head>
</head>
<body>
<div id="app">{{msgTranslated}}</div>
</body>
</html>
javascript file injected:
new Vue({
el: '#app',
data: {
msgTranslated: chrome.i18n.getMessage("message")
}
})

How can I let a user download multiple files when a button is clicked?

So I have a httpd server running which has links to a bunch of files. Lets say the user selects three files from a file list to download and they're located at:
mysite.com/file1
mysite.com/file2
mysite.com/file3
When they click the download button I want them to download these three files from the links above.
My download button looks something like:
var downloadButton = new Ext.Button({
text: "Download",
handler: function(){
//download the three files here
}
});
The best way to do this is to have your files zipped and link to that:
The other solution can be found here: How to make a link open multiple pages when clicked
Which states the following:
HTML:
Download
JS:
$('a.yourlink').click(function(e) {
e.preventDefault();
window.open('mysite.com/file1');
window.open('mysite.com/file2');
window.open('mysite.com/file3');
});
Having said this, I would still go with zipping the file, as this implementation requires JavaScript and can also sometimes be blocked as popups.
This was the method which worked best for me and didn't open up new tabs, but just downloaded the files/images I required:
var filesForDownload = [];
filesForDownload( { path: "/path/file1.txt", name: "file1.txt" } );
filesForDownload( { path: "/path/file2.jpg", name: "file2.jpg" } );
filesForDownload( { path: "/path/file3.png", name: "file3.png" } );
filesForDownload( { path: "/path/file4.txt", name: "file4.txt" } );
$jq('input.downloadAll').click( function( e )
{
e.preventDefault();
var temporaryDownloadLink = document.createElement("a");
temporaryDownloadLink.style.display = 'none';
document.body.appendChild( temporaryDownloadLink );
for( var n = 0; n < filesForDownload.length; n++ )
{
var download = filesForDownload[n];
temporaryDownloadLink.setAttribute( 'href', download.path );
temporaryDownloadLink.setAttribute( 'download', download.name );
temporaryDownloadLink.click();
}
document.body.removeChild( temporaryDownloadLink );
} );
I fond that executing click() event on a element inside a for loop for multiple files download works only for limited number of files (10 files in my case). The only reason that would explain this behavior that made sense to me, was speed/intervals of downloads executed by click() events.
I figure out that, if I slow down execution of click() event, then I will be able to downloads all files.
This is solution that worked for me.
var urls = [
'http://example.com/file1',
'http://example.com/file2',
'http://example.com/file3'
]
var interval = setInterval(download, 300, urls);
function download(urls) {
var url = urls.pop();
var a = document.createElement("a");
a.setAttribute('href', url);
a.setAttribute('download', '');
a.setAttribute('target', '_blank');
a.click();
if (urls.length == 0) {
clearInterval(interval);
}
}
I execute download event click() every 300ms. When there is no more files to download urls.length == 0 then, I execute clearInterval on interval function to stop downloads.
You can either:
Zip the selected files and return the one zipped file.
Open multiple pop-ups each prompting for a download.
Note - option one is objectively better.
Edit
Found an option three: https://stackoverflow.com/a/9425731/1803682
I've solved this a different way by using window.location. It works in Chrome, which fortunately is the only browser I had to support. Might be useful to someone. I'd initally used Dan's answer, which also needed the timeout I've used here or it only downloaded one file.
var linkArray = [];
linkArray.push("http://example.com/downloadablefile1");
linkArray.push("http://example.com/downloadablefile2");
linkArray.push("http://example.com/downloadablefile3");
function (linkArray) {
for (var i = 0; i < linkArray.length; i++) {
setTimeout(function (path) { window.location = path; }, 200 + i * 200, linkArray[i]);
}
};
<!DOCTYPE html>
<html ng-app='app'>
<head>
<title>
</title>
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body ng-cloack>
<div class="container" ng-controller='FirstCtrl'>
<table class="table table-bordered table-downloads">
<thead>
<tr>
<th>Select</th>
<th>File name</th>
<th>Downloads</th>
</tr>
</thead>
<tbody>
<tr ng-repeat = 'tableData in tableDatas'>
<td>
<div class="checkbox">
<input type="checkbox" name="{{tableData.name}}" id="{{tableData.name}}" value="{{tableData.name}}" ng-model= 'tableData.checked' ng-change="selected()">
</div>
</td>
<td>{{tableData.fileName}}</td>
<td>
<a target="_self" id="download-{{tableData.name}}" ng-href="{{tableData.filePath}}" class="btn btn-success pull-right downloadable" download>download</a>
</td>
</tr>
</tbody>
</table>
<a class="btn btn-success pull-right" ng-click='downloadAll()'>download selected</a>
<p>{{selectedone}}</p>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<script src="script.js"></script>
</body>
</html>
app.js
var app = angular.module('app', []);
app.controller('FirstCtrl', ['$scope','$http', '$filter', function($scope, $http, $filter){
$scope.tableDatas = [
{name: 'value1', fileName:'file1', filePath: 'data/file1.txt', selected: true},
{name: 'value2', fileName:'file2', filePath: 'data/file2.txt', selected: true},
{name: 'value3', fileName:'file3', filePath: 'data/file3.txt', selected: false},
{name: 'value4', fileName:'file4', filePath: 'data/file4.txt', selected: true},
{name: 'value5', fileName:'file5', filePath: 'data/file5.txt', selected: true},
{name: 'value6', fileName:'file6', filePath: 'data/file6.txt', selected: false},
];
$scope.application = [];
$scope.selected = function() {
$scope.application = $filter('filter')($scope.tableDatas, {
checked: true
});
}
$scope.downloadAll = function(){
$scope.selectedone = [];
angular.forEach($scope.application,function(val){
$scope.selectedone.push(val.name);
$scope.id = val.name;
angular.element('#'+val.name).closest('tr').find('.downloadable')[0].click();
});
}
}]);
plunker example: https://plnkr.co/edit/XynXRS7c742JPfCA3IpE?p=preview
This works in all browsers (IE11, Firefox, Microsoft Edge, Chrome and Chrome Mobile) My documents are in multiple select elements. The browsers seem to have issues when you try to do it too fast... So I used a timeout.
<select class="document">
<option val="word.docx">some word document</option>
</select>
//user clicks a download button to download all selected documents
$('#downloadDocumentsButton').click(function () {
var interval = 1000;
//select elements have class name of "document"
$('.document').each(function (index, element) {
var doc = $(element).val();
if (doc) {
setTimeout(function () {
window.location = doc;
}, interval * (index + 1));
}
});
});
This solution uses promises:
function downloadDocs(docs) {
docs[0].then(function (result) {
if (result.web) {
window.open(result.doc);
}
else {
window.location = result.doc;
}
if (docs.length > 1) {
setTimeout(function () { return downloadDocs(docs.slice(1)); }, 2000);
}
});
}
$('#downloadDocumentsButton').click(function () {
var files = [];
$('.document').each(function (index, element) {
var doc = $(element).val();
var ext = doc.split('.')[doc.split('.').length - 1];
if (doc && $.inArray(ext, docTypes) > -1) {
files.unshift(Promise.resolve({ doc: doc, web: false }));
}
else if (doc && ($.inArray(ext, webTypes) > -1 || ext.includes('?'))) {
files.push(Promise.resolve({ doc: doc, web: true }));
}
});
downloadDocs(files);
});
This is the easiest way I have found to download multiple files.
$('body').on('click','.download_btn',function(){
downloadFiles([
['File1.pdf', 'File1-link-here'],
['File2.pdf', 'File2-link-here'],
['File3.pdf', 'File3-link-here'],
['File4.pdf', 'File4-link-here']
]);
})
function downloadFiles(files){
if(files.length == 0){
return;
}
file = files.pop();
var Link = $('body').append('');
Link[0].click();
Link.remove();
downloadFiles(files);
}
This should be work for you.
Thank You.
you could go for the iframe
NOTE: though this is one of the few ways to download multiple files at once, without creating pop-up, this won't work with files that can be rendered in browser. check js comment
NOTE 2: some browsers may ask for permission to download multiple files from the same page
function download(){
const links = ["mysite.com/file1", "mysite.com/file2", "mysite.com/file3"]
// only works with files that don't render in browser
// ie: not video, not text, not photo
for(let i = 0; i < links.length; i++) {
var frame = document.createElement("iframe");
frame.src = links[i];
frame["download"] = 1
document.body.appendChild(frame);
}
}
iframe{
display: none;
}
<body>
<div onclick="download()">Download</div>
</body>
this solution works perfectly fine for me
var downloadButton = new Ext.Button({
text: "Download",
handler: function () {
/** #type {Array<string>} URLS */
const URLS = [
"mysite.com/file1 ",
"mysite.com/file2",
"mysite.com/file3",
];
for (let x = 0; x < URLS.length; x++) {
/** #type {string} URL */
const URL = URLS[x];
/** #type {HTMLLinkElement} LINK */
const LINK = document.createElement('a');
LINK.href = URL;
LINK.setAttribute('download', 'download');
document.body.appendChild(LINK);
LINK.click();
LINK.parentNode.removeChild(LINK);
window.URL.revokeObjectURL(URL);
}
}
});

Categories