Find text (ISBN) on page and attach link on it in JS - firefox

For a browser (Firefox) addon I need to find ISBN's on certain pages (such as: amazon.com, book.com).
I need to find those DOM elemets containing the ISBN and manipulate them with a link, which sends the ISBN to a REST Webservice for further logic.
Since I'm new to JavaScript I don't have any idea how to get into this because the homepages differ in how they display the ISBN.
This is the current implementation:
var self = require("sdk/self");
// a dummy function, to show how tests work.
// to see how to test this function, look at test/test-index.js
function dummy(text, callback) {
callback(text);
}
exports.dummy = dummy;
var tag = "body"
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
var pageMod = require("sdk/page-mod");
var data = require("sdk/self").data;
var button = buttons.ActionButton({
id: "mozilla-link",
label: "Visit Mozilla",
icon: {
"16": "./icon-16.png",
"32": "./icon-32.png",
"64": "./icon-64.png"
},
onClick: handleClick
});
function handleClick(state) {
//tabs.open("http://www.mozilla.org/");
tabs.open("http://www.amazon.com");
}
pageMod.PageMod({
include: "*",
contentScriptFile: data.url("modify-content.js"),
onAttach: function(worker) {
worker.port.emit("getElements", tag);
worker.port.on("gotElement", function(elementContent) {
console.log(elementContent);
});
}
});
This is the modify-content.js
self.port.on("getElements", function(tag) {
var elements = document.getElementsByTagName(tag);
for (var i = 0; i < elements.length; i++) {
var isbn = elements[i].innerText.match("(ISBN[-]*(1[03])*[ ]*(: ){0,1})*(([0-9Xx][- ]*){13}|([0-9Xx][- ]*){10})");
if(isbn != undefined){
//self.port.emit("gotElement", elements[i].innerHTML);
console.log(isbn);
}
self.port.emit("gotElement", elements[i].innerHTML);
}
});
The ISBN's are found. But how do I manipulate the DOM Element surrounding the ISBN?

If I understand your question correctly, you're having trouble translating back from a innerText regexp match, which is a string, to the nearest html element of that match.
The difficult part is that the DOM is a tree. So if a <div> has a <p> which has a <span> which has an ISBN number, all of those elements' innerText will contain that ISBN number. We want to reach the "deepest" element that contains a match, ignoring all of its parents.
One (rather expensive, brute-force) way of doing this, is by recursively looping through your elements, layer by layer.
When there is a match in the innerText, we'll look at every child element.
If none of the children has a match, we return the current element because it's the last known DOM element that had a match
Other than performance, I think there are still edge cases that won't match. For example, if there's a <span> that contains three, comma separated, ISBN numbers. You'll probably have to add one last layer: a layer that creates new elements around each individual string.
var isbnRegExp = new RegExp("(ISBN[-]*(1[03])*[ ]*(: ){0,1})*(([0-9Xx][- ]*){13}|([0-9Xx][- ]*){10})");
var start = document.body;
var findDeepestMatch = function(el, regexp, result) {
result = result || [];
if (regexp.test(el.innerHTML)) {
var childMatches = Array.from(el.children)
.filter(child => regexp.test(child.innerHTML));
// Case 1: there's a match here, but not in any of the children: return el
if (childMatches.length === 0) {
result.push(el);
} else {
childMatches.forEach(child => findDeepestMatch(child, regexp, result));
}
}
return result;
};
console.time("Find results")
var results = findDeepestMatch(start, isbnRegExp);
console.timeEnd("Find results")
var makeISBN = el => {
el.classList.add("isbn");
el.addEventListener("click", () => console.log(el.innerText));
};
results.forEach(makeISBN);
.isbn { color: green; border-radius: 3px; background: rgba(0,255,0,0.2); cursor: pointer; }
<div>
<div>
<ul>
<li>
Some book: <span>978-0451524935</span>
</li>
<li>
Some nested book: <span><strong>ISBN</strong>-978-0451524935</span>
</li>
<li>
Some phone number: <span>0012-34-5678909</span> <code><-- regexp problem?</code>
</li>
<li>
Some email: <span>abc#def.gh</span>
</li>
</ul>
</div>
<div>
Here are no isbn numbers
</div>
Here's one <a><strong>ISBN</strong>-<em>978</em>-0451524935</a>
</div>

Related

Implement Aria 1.1

I am trying to implement the example 1 provided by w3c.org. The URL is https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html. It keeps giving me aria undefined error on line (var ex1Combobox = new aria.ListboxCombobox). Here is the code:
<!DOCTYPE html>
<html>
<body>
<label for="ex1-input"
id="ex1-label"
class="combobox-label">
Choice 1 Fruit or Vegetable
</label>
<div class="combobox-wrapper">
<div role="combobox"
aria-expanded="false"
aria-owns="ex1-listbox"
aria-haspopup="listbox"
id="ex1-combobox">
<input type="text"
aria-autocomplete="list"
aria-controls="ex1-listbox"
id="ex1-input">
</div>
<ul aria-labelledby="ex1-label"
role="listbox"
id="ex1-listbox"
class="listbox hidden">
</ul>
</div>
<script>
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* ARIA Combobox Examples
*/
var FRUITS_AND_VEGGIES = [
'Apple',
'Artichoke',
'Asparagus',
'Banana',
'Beets',
'Bell pepper',
'Broccoli',
'Brussels sprout',
'Cabbage',
'Carrot',
'Cauliflower',
'Celery',
'Chard',
'Chicory',
'Corn',
'Cucumber',
'Daikon',
'Date',
'Edamame',
'Eggplant',
'Elderberry',
'Fennel',
'Fig',
'Garlic',
'Grape',
'Honeydew melon',
'Iceberg lettuce',
'Jerusalem artichoke',
'Kale',
'Kiwi',
'Leek',
'Lemon',
'Mango',
'Mangosteen',
'Melon',
'Mushroom',
'Nectarine',
'Okra',
'Olive',
'Onion',
'Orange',
'Parship',
'Pea',
'Pear',
'Pineapple',
'Potato',
'Pumpkin',
'Quince',
'Radish',
'Rhubarb',
'Shallot',
'Spinach',
'Squash',
'Strawberry',
'Sweet potato',
'Tomato',
'Turnip',
'Ugli fruit',
'Victoria plum',
'Watercress',
'Watermelon',
'Yam',
'Zucchini'
];
function searchVeggies (searchString) {
var results = [];
for (var i = 0; i < FRUITS_AND_VEGGIES.length; i++) {
var veggie = FRUITS_AND_VEGGIES[i].toLowerCase();
if (veggie.indexOf(searchString.toLowerCase()) === 0) {
results.push(FRUITS_AND_VEGGIES[i]);
}
}
return results;
}
/**
* #function onload
* #desc Initialize the combobox examples once the page has loaded
*/
window.addEventListener('load', function () {
var ex1Combobox = new aria.ListboxCombobox(
document.getElementById('ex1-combobox'),
document.getElementById('ex1-input'),
document.getElementById('ex1-listbox'),
searchVeggies,
false
);
var ex2Combobox = new aria.ListboxCombobox(
document.getElementById('ex2-combobox'),
document.getElementById('ex2-input'),
document.getElementById('ex2-listbox'),
searchVeggies,
true
);
var ex3Combobox = new aria.ListboxCombobox(
document.getElementById('ex3-combobox'),
document.getElementById('ex3-input'),
document.getElementById('ex3-listbox'),
searchVeggies,
true,
function () {
// on show
document.getElementById('ex3-combobox-arrow')
.setAttribute('aria-label', 'Hide vegetable options');
},
function () {
// on hide
document.getElementById('ex3-combobox-arrow')
.setAttribute('aria-label', 'Show vegetable options');
}
);
document.getElementById('ex3-combobox-arrow').addEventListener(
'click',
function () {
if (ex3Combobox.shown) {
document.getElementById('ex3-input').focus();
ex3Combobox.hideListbox();
}
else {
document.getElementById('ex3-input').focus();
ex3Combobox.updateResults(true);
}
}
);
});
</script>
</body>
</html>
Any help would be appreciated.
I realize that this is an old post, but I have determined why the undefined error is occurring:
There are two js files associated with this example:
ListBox-Combox.js &
ListBox-Combo-example.js
The 'ListBox-Combo-example.js' file has event listeners for all three examples on the page
https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html.
Since you only copied the code for the first example, when the javascript attempts to find the combobox 'ex2-Combobox' it cannot find it so javascript throws an error.
You can comment out these lines in the 'ListBox-Combo-example.js' file:
);
var ex2Combobox = new aria.ListboxCombobox(
document.getElementById('ex2-combobox'),
document.getElementById('ex2-input'),
document.getElementById('ex2-listbox'),
searchVeggies,
true
);
var ex3Combobox = new aria.ListboxCombobox(
document.getElementById('ex3-combobox'),
document.getElementById('ex3-input'),
document.getElementById('ex3-listbox'),
searchVeggies,
true,
and replace with a comma. That should solve the problem.

Is there a way to make the output image of ckeditor responsive?

I have ckeditor wherein images are responsive on the editor itself but the output which in blade.php (html) are not responsive..
I expect that after editing in the ckeditor image would resize according to the container usually bootstrap..
You could use the .on() method of CKEDITOR.
Ex.
CKEDITOR
.on: {
instanceReady: function() {
this.dataProcessor.htmlFilter.addRules( {
elements: {
img: function( el ) {
// Add an attribute.
if ( !el.attributes.alt )
el.attributes.alt = 'An image';
// Add some class.
el.addClass( 'newsleft' );
}
}
} );
}
}
Edit: Using bootstrap you need to use el.addClass('img-fluid'); for responsive images.
Actually I have these on config.js
CKEDITOR.on('instanceReady', function (ev) {
ev.editor.dataProcessor.htmlFilter.addRules( {
elements : {
img: function( el ) {
// Add bootstrap "img-responsive" class to each inserted image
el.addClass('img-fluid');
// Remove inline "height" and "width" styles and
// replace them with their attribute counterparts.
// This ensures that the 'img-responsive' class works
var style = el.attributes.style;
if (style) {
// Get the width from the style.
var match = /(?:^|\s)width\s*:\s*(\d+)px/i.exec(style),
width = match && match[1];
// Get the height from the style.
match = /(?:^|\s)height\s*:\s*(\d+)px/i.exec(style);
var height = match && match[1];
// Replace the width
if (width) {
el.attributes.style = el.attributes.style.replace(/(?:^|\s)width\s*:\s*(\d+)px;?/i, '');
el.attributes.width = width;
}
// Replace the height
if (height) {
el.attributes.style = el.attributes.style.replace(/(?:^|\s)height\s*:\s*(\d+)px;?/i, '');
el.attributes.height = height;
}
}
// Remove the style tag if it is empty
if (!el.attributes.style)
delete el.attributes.style;
}
}
});
});
What is the problem with the above code? Is it for ckeditor itself or the output also in the html. Is it the same with what you want me to do? The {{$lesson->body_content}} displays the edit file from ckeditor as shown below. I want all image to automatically resize into smaller one because their too big overlapping the container..
<div class="card-body" >
<br>
<p class="text text-justify">
Back to:
<b>{{$lesson->course->title}}</b></h3>
</p>
<p class="text text-justify">
{!! $lesson->body_content !!}
</p>

React Search Filter of Objects not filtering

I'm trying to create a search filter that will filter through facility names that lives in an array of objects.If I hard code an array into the state the filter works, but I need it to drab the info from props. The filtered list is being generated and showing all of the names on the screen but when I type it the textbox to filter nothing happens. What have I overlooked?
class FacilitySearch extends React.Component {
constructor(props) {
super(props);
this.state = {
search: ""
};
}
componentDidMount() {
this.props.dispatch(actions.getFacilitiesList());
}
//The subsr limits the # of characters a user can enter into the seach box
updateSearch = event => {
this.setState({ search: event.target.value.substr(0, 10) });
};
render() {
if (!this.props.facilityList) {
return <div>Loading...</div>
}
let filteredList = this.props.facilityList;
filteredList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
return (
<div>
<input
type="text"
value={this.state.search}
onChange={this.updateSearch.bind(this)}
placeholder="Enter Text Here..."
/>
<ul>
{filteredList.map(facility => {
return <li key={facility.generalIdPk}>{facility.facilityName}</li>;
})}
</ul>
</div>
);
}
}
const mapStateToProps = state => ({
facilityList: state.facilityList.facilityList
});
export default connect(mapStateToProps)(FacilitySearch)
The problem is that you are not storing the return value of filter in any variable.
You should do something like:
let filteredList = this.props.facilityList.filter(facility => {
return facility.facilityName.toLowerCase().indexOf(this.state.search.toLowerCase()) !== -1;
});
From MDN:
The filter() method creates a new array with all elements that pass the test implemented by the provided function.

How to determine number of children in a slot

Is there anyway to know how many children a named slot contains? In my Stencil component I have something like this in my render function:
<div class="content">
<slot name="content"></slot>
</div>
What I want to do is style the div.content differently depending on how many children are inside the slot. If there are no children in the slot, then div.content's style.display='none', otherwise, I have a bunch of styles applied to div.content that make the children appear correctly on the screen.
I tried doing:
const divEl = root.querySelector( 'div.content' );
if( divEl instanceof HTMLElement ) {
const slotEl = divEl.firstElementChild;
const hasChildren = slotEl && slotEl.childElementCount > 0;
if( !hasChildren ) {
divEl.style.display = 'none';
}
}
however this is always reporting hasChildren = false even when I have items inserted into the slot.
If you are querying the host element you will get all the slotted content inside of it. That means that the host element's children are going to be all the content that will be injected into the slot.
For example try to use the following code to see it in action:
import {Component, Element, State} from '#stencil/core';
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Element() host: HTMLElement;
#State() childrenData: any = {};
componentDidLoad() {
let slotted = this.host.children;
this.childrenData = { hasChildren: slotted && slotted.length > 0, numberOfChildren: slotted && slotted.length };
}
render() {
return (
<div class="content">
<slot name="content"></slot>
<div>
Slot has children: {this.childrenData.hasChildren ? 'true' : 'false'}
</div>
<div>
Number of children: {this.childrenData.numberOfChildren}
</div>
</div>);
}
}
The accepted solution is actually not the correct way to do it. Even the code example is wrong. It is using a named slot name="content". Only elements with slot="content" attribute from the light DOM will be slotted into that slot; hence simply checking this.host.children is not sufficient at all.
Instead, you should work with the slotchange event (which also has the benefit of properly reflecting dynamic changes):
import {Component, Element, State} from '#stencil/core';
export type TSlotInfo = {
hasSlottedElements?: boolean;
numberOfSlottedElements?: number;
}
#Component({
tag: 'my-component',
shadow: true
})
export class MyComponent {
#Element() host: HTMLElement;
#State() slotInfo: TSlotInfo = {};
handleSlotChange = (event: Event) => {
let assignedElementCount = event.currentTarget.assignedElements().length;
this.slotInfo = {
hasSlottedElements: Boolean(assignedElementCount),
numberOfSlottedElements: assignedElementCount,
}
}
render() {
return (
<div class="content">
<slot name="content" onslotchange={this.handleSlotChange}></slot>
<div>
Slot is populated: {this.slotInfo.hasSlottedElements}
</div>
<div>
Number of slotted elements: {this.slotInfo.numberOfSlottedElements}
</div>
</div>);
}
}

how to make angular filters customizable and how to use them in javascript

I have an array on my controller
$scope.arr = [......], a
nd in html I want to do
ng-repeat = "item in arr | color:'blue'" //this line works, filter done in the app.filter way.
where color is an attribute of all objects in arr.
How Do I make this color:'blue' customizable and sub in color:'red'?
also if I want to do controller filtering instead of html, what would be the syntax, right now I have
$scope.filteredArr = $filter('color')($scope.arr,'blue');
which is giving error
http://jsfiddle.net/x3azn/YpMKX/
posted fiddle, please remove -1
You can customize color:'blue' with any expression in the format filter:expression, so something like color:myColor would work fine provided a color filter has been defined and myColor exists in the current scope.
In your controller, you can do the same.
$scope.filteredArr = $filter('color')($scope.arr,myColor);
Here is an example based on your jsFiddle example.
Javascript:
angular.module('app', [])
.filter('eyecolor', function () {
return function ( people, color ) {
var arr = [];
for ( var i = people.length; i--; ) {
if ( people[i].eyeColor === color ) {
arr.push( people[i] );
}
}
return arr;
}
})
.controller('ctrl', function ($scope, $filter) {
$scope.desiredColor = 'Brown';
$scope.people = [
{
name: 'Bob',
eyeColor: 'Brown'
}, {
name: 'Katherine',
eyeColor: 'Yellow'
}, {
name: 'Chun',
eyeColor: 'Black'
}
];
$scope.peopleFilter = $filter('eyecolor')( $scope.people, $scope.desiredColor );
});
Html:
<div ng-app="app">
<div ng-controller="ctrl">Color Desired:
<input ng-model="desiredColor" /><br/>
<div ng-repeat="person in people | eyecolor: desiredColor">
HTML filter: {{person.name}} has eye color {{person.eyeColor}}</div>
<div ng-repeat="person in peopleFilter">
Controller filter: {{ person.name }} has eye color {{ person.eyeColor }}</div>
</div>
</div>

Resources