initial commit of starter install drupal 7

This commit is contained in:
Bachir Soussi Chiadmi
2015-02-21 17:21:40 +01:00
commit eeba51cea6
6113 changed files with 1249218 additions and 0 deletions
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,84 @@
/* Stylesheet for Better Autocomplete jQuery plugin by Didrik Nordström */
/**
* Do NOT edit this file! Instead, include another stylesheet with your custom
* overrides and extensions. Copy the selectors you need to edit and place them
* in your CSS file. And remember to include that file AFTER this file.
*/
/* Reset styles for lists */
.better-autocomplete,
.better-autocomplete > .result {
list-style: none;
padding: 0;
margin: 0;
background: none;
}
.better-autocomplete {
text-align: left;
font-family: 'Lucida Grande', Helvetica, Arial, Verdana, sans-serif;
overflow: auto;
cursor: default;
background: white;
border-bottom: 1px solid #bfbfbf;
}
/* Groups */
.better-autocomplete > .group {
position: relative;
padding: 3px;
text-align: center;
background: #e7e7e7;
border-color: #bfbfbf;
border-width: 1px 1px 0 1px;
border-style: solid;
}
.better-autocomplete > .group > h3 {
font-size: 11px;
color: #555;
padding: 0;
margin: 0;
}
/* Results */
.better-autocomplete > .result {
position: relative;
padding: 5px 7px;
background: white;
border-color: #bfbfbf;
border-width: 1px 1px 0 1px;
border-style: solid;
}
.better-autocomplete > .result > h4 {
font-size: 12px;
color: #3c3c3c;
padding: 0;
margin: 0 0 2px 0;
}
.better-autocomplete > .result > p {
font-size: 11px;
line-height: 1.3;
color: #6f6f6f;
padding: 0;
margin: 0;
}
/* Highlighting */
.better-autocomplete > .result.highlight {
background: #0075ba;
border-top-color: #1f4a64;
}
.better-autocomplete > .result.highlight > h4,
.better-autocomplete > .result.highlight > p {
color: white;
}
@@ -0,0 +1,943 @@
/**
* @fileOverview Better Autocomplete is a flexible jQuery plugin which offers
* rich text autocompletion, both from local and remote sources.
*
* @author Didrik Nordström, http://betamos.se/
*
* @version v1.0-dev
*
* @requires
* <ul><li>
* jQuery 1.4+
* </li><li>
* IE7+ or any decent webkit/gecko-based web browser
* </li></ul>
*
* @preserve Better Autocomplete v1.0-dev
* https://github.com/betamos/Better-Autocomplete
*
* Copyright 2011, Didrik Nordström, http://betamos.se/
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Requires jQuery 1.4+
* http://jquery.com/
*/
/**
* Create or alter an autocomplete object instance that belongs to
* the elements in the selection. Make sure there are only text field elements
* in the selection.
*
* @constructor
*
* @name jQuery.betterAutocomplete
*
* @param {String} method
* Should be one of the following:
* <ul><li>
* init: Initiate Better Autocomplete instances on the text input elements
* in the current jQuery selection. They are enabled by default. The other
* parameters are then required.
* </li><li>
* enable: In this jQuery selection, reenable the Better Autocomplete
* instances.
* </li><li>
* disable: In this jQuery selection, disable the Better Autocomplete
* instances.
* </li><li>
* destroy: In this jQuery selection, destroy the Better Autocomplete
* instances. It will not be possible to reenable them after this.
* </li></ul>
*
* @param {String|Object} [resource]
* If String, it will become the path for a remote resource. If not, it will
* be treated like a local resource. The path should provide JSON objects
* upon HTTP requests.
*
* @param {Object} [options]
* An object with configurable options:
* <ul><li>
* charLimit: (default=3 for remote or 1 for local resource) The minimum
* number of chars to do an AJAX call. A typical use case for this limit is
* to reduce server load.
* </li><li>
* delay: (default=350) The time in ms between last keypress and AJAX call.
* Typically used to prevent looking up irrelevant strings while the user
* is still typing. Only relevant for remote resources.
* </li><li>
* caseSensitive: (default=false) If the search should be case sensitive.
* If false, query strings will be converted to lowercase.
* </li><li>
* cacheLimit: (default=256 for remote or 0 for local resource) The maximum
* number of result objects to store in the cache. This option reduces
* server load if the user deletes characters to check back on previous
* results. To disable caching of previous results, set this option to 0.
* </li><li>
* remoteTimeout: (default=10000) The timeout for remote (AJAX) calls.
* </li><li>
* crossOrigin: (default=false) Set to true if cross origin requests will
* be performed, i.e. that the remote URL has a different domain. This will
* force Internet Explorer to use "jsonp" instead of "json" as datatype.
* </li><li>
* selectKeys: (default=[9, 13]) The key codes for keys which will select
* the current highlighted element. The defaults are tab, enter.
* </li><li>
* autoHighlight: (default=true) Automatically highlight the first result.
* </li></ul>
*
* @param {Object} [callbacks]
* An object containing optional callback functions on certain events. See
* {@link callbacks} for details. These callbacks should be used when
* customization of the default behavior of Better Autocomplete is required.
*
* @returns {Object}
* The jQuery object with the same element selection, for chaining.
*/
(function($) {
$.fn.betterAutocomplete = function(method) {
/*
* Each method expects the "this" object to be a valid DOM text input node.
* The methods "enable", "disable" and "destroy" expects an instance of a
* BetterAutocomplete object as their first argument.
*/
var methods = {
init: function(resource, options, callbacks) {
var $input = $(this),
bac = new BetterAutocomplete($input, resource, options, callbacks);
$input.data('better-autocomplete', bac);
bac.enable();
},
enable: function(bac) {
bac.enable();
},
disable: function(bac) {
bac.disable();
},
destroy: function(bac) {
bac.destroy();
}
}, args = Array.prototype.slice.call(arguments, 1);
// Method calling logic
this.each(function() {
switch (method) {
case 'init':
methods[method].apply(this, args);
break;
case 'enable':
case 'disable':
case 'destroy':
var bac = $(this).data('better-autocomplete');
if (bac instanceof BetterAutocomplete) {
methods[method].call(this, bac);
}
break;
default:
$.error(['Method', method,
'does not exist in jQuery.betterAutocomplete.'].join(' '));
}
});
// Maintain chainability
return this;
};
/**
* The BetterAutocomplete constructor function. Returns a BetterAutocomplete
* instance object.
*
* @private @constructor
* @name BetterAutocomplete
*
* @param {Object} $input
* A single input element wrapped in jQuery.
*/
var BetterAutocomplete = function($input, resource, options, callbacks) {
var lastRenderedQuery = '',
cache = {}, // Key-valued caching of search results
cacheOrder = [], // Array of query strings, in the order they are added
cacheSize = 0, // Keep count of the cache's size
timer, // Used for options.delay
activeRemoteCalls = [], // A flat array of query strings that are pending
disableMouseHighlight = false, // Suppress the autotriggered mouseover event
inputEvents = {},
isLocal = ($.type(resource) != 'string'),
$results = $('<ul />').addClass('better-autocomplete'),
hiddenResults = true, // $results are hidden
preventBlurTimer = null; // IE bug workaround, see below in code.
options = $.extend({
charLimit: isLocal ? 1 : 3,
delay: 350, // milliseconds
caseSensitive: false,
cacheLimit: isLocal ? 0 : 256, // Number of result objects
remoteTimeout: 10000, // milliseconds
crossOrigin: false,
selectKeys: [9, 13], // [tab, enter]
autoHighlight: true // Automatically highlight the topmost result
}, options);
callbacks = $.extend({}, defaultCallbacks, callbacks);
callbacks.insertSuggestionList($results, $input);
inputEvents.focus = function() {
// If the blur timer is active, a redraw is redundant.
preventBlurTimer || redraw(true);
};
inputEvents.blur = function() {
// If the blur prevention timer is active, refocus the input, since the
// blur event can not be cancelled.
if (preventBlurTimer) {
$input.focus();
}
else {
// The input has already lost focus, so redraw the suggestion list.
redraw();
}
};
inputEvents.keydown = function(event) {
var index = getHighlightedIndex();
// If an arrow key is pressed and a result is highlighted
if ($.inArray(event.keyCode, [38, 40]) >= 0 && $results.children().length > 0) {
var newIndex,
size = $('.result', $results).length;
switch (event.keyCode) {
case 38: // Up arrow
newIndex = Math.max(0, index - 1);
break;
case 40: // Down arrow
newIndex = Math.min(size - 1, index + 1);
break;
}
disableMouseHighlight = true;
setHighlighted(newIndex, 'key', true);
return false;
}
// A select key has been pressed
else if ($.inArray(event.keyCode, options.selectKeys) >= 0 &&
!event.shiftKey && !event.ctrlKey && !event.altKey &&
!event.metaKey) {
select();
return event.keyCode == 9; // Never cancel tab
}
};
inputEvents.keyup = inputEvents.click = reprocess;
$results.delegate('.result', {
// When the user hovers a result with the mouse, highlight it.
mouseenter: function() {
if (disableMouseHighlight) {
return;
}
setHighlighted($('.result', $results).index($(this)), 'mouse');
},
mousemove: function() {
// Enable mouseover again.
disableMouseHighlight = false;
},
mousedown: function() {
select();
return false;
}
});
// Prevent blur when clicking on group titles, scrollbars etc.,
// This event is triggered after the others because of bubbling.
$results.mousedown(function() {
// Bug in IE where clicking on scrollbar would trigger a blur event for the
// input field, despite using preventDefault() on the mousedown event.
// This workaround locks the blur event on the input for a small time.
clearTimeout(preventBlurTimer);
preventBlurTimer = setTimeout(function() {
preventBlurTimer = null;
}, 50);
return false;
});
// If auto highlight is off, remove highlighting
$results.mouseleave(function() {
if (!options.autoHighlight) {
setHighlighted(-1);
}
});
/*
* PUBLIC METHODS
*/
/**
* Enable this instance.
*/
this.enable = function() {
// Turn off the browser's autocompletion
$input
.attr('autocomplete', 'OFF')
.attr('aria-autocomplete', 'list');
$input.bind(inputEvents);
};
/**
* Disable this instance.
*/
this.disable = function() {
$input
.removeAttr('autocomplete')
.removeAttr('aria-autocomplete');
$results.hide();
$input.unbind(inputEvents);
};
/**
* Disable and remove this instance. This instance should not be reused.
*/
this.destroy = function() {
$results.remove();
$input.unbind(inputEvents);
$input.removeData('better-autocomplete');
};
/*
* PRIVATE METHODS
*/
/**
* Add an array of results to the cache. Internal methods always reads from
* the cache, so this method must be invoked even when caching is not used,
* e.g. when using local results. This method automatically clears as much of
* the cache as required to fit within the cache limit.
*
* @param {String} query
* The query to set the results to.
*
* @param {Array[Object]} results
* The array of results for this query.
*/
var cacheResults = function(query, results) {
cacheSize += results.length;
// Now reduce size until it fits
while (cacheSize > options.cacheLimit && cacheOrder.length) {
var key = cacheOrder.shift();
cacheSize -= cache[key].length;
delete cache[key];
}
cacheOrder.push(query);
cache[query] = results;
};
/**
* Set highlight to a specific result item
*
* @param {Number} index
* The result item's index, or negative if highlight should be removed.
*
* @param {String} [trigger]
* What triggered the highlight: "mouse", "key" or "auto". If index is
* negative trigger may be omitted.
*
* @param {Boolean} [autoScroll]
* (default=false) If scrolling of the results list should be automated.
*/
var setHighlighted = function(index, trigger, autoScroll) {
//console.log('Index: '+index)
var prevIndex = getHighlightedIndex(),
$resultList = $('.result', $results);
//console.log('prevIndex: '+prevIndex)
$resultList.removeClass('highlight');
if (index < 0) {
return
}
$resultList.eq(index).addClass('highlight')
if (prevIndex != index) {
var result = getResultByIndex(index);
callbacks.highlight(result, $input, trigger);
}
// Scrolling
var up = index == 0 || index < prevIndex,
$scrollTo = $resultList.eq(index);
if (!autoScroll) {
return;
}
// Scrolling up, then make sure to show the group title
if ($scrollTo.prev().is('.group') && up) {
$scrollTo = $scrollTo.prev();
}
// Is $scrollTo partly above the visible region?
if ($scrollTo.position().top < 0) {
$results.scrollTop($scrollTo.position().top + $results.scrollTop());
}
// Or is it partly below the visible region?
else if (($scrollTo.position().top + $scrollTo.outerHeight()) >
$results.height()) {
$results.scrollTop($scrollTo.position().top + $results.scrollTop() +
$scrollTo.outerHeight() - $results.height());
}
};
/**
* Retrieve the index of the currently highlighted result item
*
* @returns {Number}
* The result's index or -1 if no result is highlighted.
*/
var getHighlightedIndex = function() {
var res = $('.result.highlight', $results)
ind= $('.result', $results).index(res);
return ind
};
/**
* Retrieve the result object with the specific position in the results list
*
* @param {Number} index
* The index of the item in the current result list.
*
* @returns {Object}
* The result object or null if index out of bounds.
*/
var getResultByIndex = function(index) {
var $result = $('.result', $results).eq(index);
if (!$result.length) {
return; // No selectable element
}
return $result.data('result');
};
/**
* Select the current highlighted element, if any.
*/
var select = function() {
var highlighted = getHighlightedIndex(),
result = getResultByIndex(highlighted);
callbacks.select(result, $input);
// Redraw again, if the callback changed focus or content
reprocess();
};
/**
* Fetch results asynchronously via AJAX.
* Errors are ignored.
*
* @param {String} query
* The query string.
*/
var fetchResults = function(query) {
// Synchronously fetch local data
if (isLocal) {
cacheResults(query, callbacks.queryLocalResults(query, resource,
options.caseSensitive));
redraw();
}
// Asynchronously fetch remote data
else {
activeRemoteCalls.push(query);
var url = callbacks.constructURL(resource, query);
callbacks.beginFetching($input);
callbacks.fetchRemoteData(url, function(data) {
var searchResults = callbacks.processRemoteData(data);
if (!$.isArray(searchResults)) {
searchResults = [];
}
cacheResults(query, searchResults);
// Remove the query from active remote calls, since it's finished
activeRemoteCalls = $.grep(activeRemoteCalls, function(value) {
return value != query;
});
if (!activeRemoteCalls.length) {
callbacks.finishFetching($input);
}
redraw();
}, options.remoteTimeout, options.crossOrigin);
}
};
/**
* Reprocess the contents of the input field, fetch data and redraw if
* necessary.
*
* @param {Object} [event]
* The event that triggered the reprocessing. Not always present.
*/
function reprocess(event) {
// If this call was triggered by an arrow key, cancel the reprocessing.
if ($.type(event) == 'object' && event.type == 'keyup' &&
$.inArray(event.keyCode, [38, 40]) >= 0) {
return;
}
var query = callbacks.canonicalQuery($input.val(), options.caseSensitive);
clearTimeout(timer);
// Indicate that timer is inactive
timer = null;
redraw();
if (query.length >= options.charLimit && !$.isArray(cache[query]) &&
$.inArray(query, activeRemoteCalls) == -1) {
// Fetching is required
$results.empty();
if (isLocal) {
fetchResults(query);
}
else {
timer = setTimeout(function() {
fetchResults(query);
timer = null;
}, options.delay);
}
}
};
/**
* Redraws the autocomplete list based on current query and focus.
*
* @param {Boolean} [focus]
* (default=false) Force to treat the input element like it's focused.
*/
var redraw = function(focus) {
var query = callbacks.canonicalQuery($input.val(), options.caseSensitive);
// The query does not exist in db
if (!$.isArray(cache[query])) {
lastRenderedQuery = null;
$results.empty();
}
// The query exists and is not already rendered
else if (lastRenderedQuery !== query) {
lastRenderedQuery = query;
renderResults(cache[query]);
if (options.autoHighlight && $('.result', $results).length > 0) {
setHighlighted(0, 'auto');
}
}
// Finally show/hide based on focus and emptiness
if (($input.is(':focus') || focus) && !$results.is(':empty')) {
$results.filter(':hidden').show() // Show if hidden
.scrollTop($results.data('scroll-top')); // Reset the lost scrolling
if (hiddenResults) {
hiddenResults = false;
callbacks.afterShow($results);
}
}
else if ($results.is(':visible')) {
// Store the scrolling position for later
$results.data('scroll-top', $results.scrollTop())
.hide(); // Hiding it resets it's scrollTop
if (!hiddenResults) {
hiddenResults = true;
callbacks.afterHide($results);
}
}
};
/**
* Regenerate the DOM content within the results list for a given set of
* results. Heavy method, use only when necessary.
*
* @param {Array[Object]} results
* An array of result objects to render.
*/
var renderResults = function(results) {
$results.empty();
var groups = {}; // Key is the group name, value is the heading element.
$.each(results, function(index, result) {
if ($.type(result) != 'object') {
return; // Continue
}
var output = callbacks.themeResult(result);
if ($.type(output) != 'string') {
return; // Continue
}
// Add the group if it doesn't exist
var group = callbacks.getGroup(result);
if ($.type(group) == 'string' && !groups[group]) {
var $groupHeading = $('<li />').addClass('group')
.append($('<h3 />').html(group))
.appendTo($results);
groups[group] = $groupHeading;
}
var $result = $('<li />').addClass('result')
.append(output)
.data('result', result) // Store the result object on this DOM element
.addClass(result.addClass);
// First groupless item
if ($.type(group) != 'string' &&
!$results.children().first().is('.result')) {
$results.prepend($result);
return; // Continue
}
var $traverseFrom = ($.type(group) == 'string') ?
groups[group] : $results.children().first();
var $target = $traverseFrom.nextUntil('.group').last();
$result.insertAfter($target.length ? $target : $traverseFrom);
});
};
};
/*
* CALLBACK METHODS
*/
/**
* These callbacks are supposed to be overridden by you when you need
* customization of the default behavior. When you are overriding a callback
* function, it is a good idea to copy the source code from the default
* callback function, as a skeleton.
*
* @name callbacks
* @namespace
*/
var defaultCallbacks = {
/**
* @lends callbacks.prototype
*/
/**
* Gets fired when the user selects a result by clicking or using the
* keyboard to select an element.
*
* <br /><br /><em>Default behavior: Inserts the result's title into the
* input field.</em>
*
* @param {Object} result
* The result object that was selected.
*
* @param {Object} $input
* The input DOM element, wrapped in jQuery.
*/
select: function(result, $input) {
$input.val(result.title);
},
/**
* Gets fired when the a result is highlighted. This may happen either
* automatically or by user action.
*
* <br /><br /><em>Default behavior: Does nothing.</em>
*
* @param {Object} result
* The result object that was selected.
*
* @param {Object} $input
* The input DOM element, wrapped in jQuery.
*
* @param {String} trigger
* The event which triggered the highlighting. Must be one of the
* following:
* <ul><li>
* "mouse": A mouseover event triggered the highlighting.
* </li><li>
* "key": The user pressed an arrow key to navigate amongst the results.
* </li><li>
* "auto": If options.autoHighlight is set, an automatic highlight of the
* first result will occur each time a new result set is rendered.
* </li></ul>
*/
highlight: function(result, $input, trigger) {
// Does nothing
},
/**
* Given a result object, theme it to HTML.
*
* <br /><br /><em>Default behavior: Wraps result.title in an h4 tag, and
* result.description in a p tag. Note that no sanitization of malicious
* scripts is done here. Whatever is within the title/description is just
* printed out. May contain HTML.</em>
*
* @param {Object} result
* The result object that should be rendered.
*
* @returns {String}
* HTML output, will be wrapped in a list element.
*/
themeResult: function(result) {
var output = [];
if ($.type(result.title) == 'string') {
output.push('<h4>', result.title, '</h4>');
}
if ($.type(result.description) == 'string') {
output.push('<p>', result.description, '</p>');
}
return output.join('');
},
/**
* Retrieve local results from the local resource by providing a query
* string.
*
* <br /><br /><em>Default behavior: Automatically handles arrays, if the
* data inside each element is either a plain string or a result object.
* If it is a result object, it will match the query string against the
* title and description property. Search is not case sensitive.</em>
*
* @param {String} query
* The query string, unescaped. May contain any UTF-8 character.
* If case insensitive, it already is lowercased.
*
* @param {Object} resource
* The resource provided in the {@link jQuery.betterAutocomplete} init
* constructor.
*
* @param {Boolean} caseSensitive
* From options.caseSensitive, the searching should be case sensitive.
*
* @returns {Array[Object]}
* A flat array containing pure result objects. May be an empty array.
*/
queryLocalResults: function(query, resource, caseSensitive) {
if (!$.isArray(resource)) {
// Per default Better Autocomplete only handles arrays
return [];
}
var results = [];
$.each(resource, function(i, value) {
switch ($.type(value)) {
case 'string': // Flat array of strings
if ((caseSensitive ? value : value.toLowerCase())
.indexOf(query) >= 0) {
// Match found
results.push({ title: value });
}
break;
case 'object': // Array of result objects
if ($.type(value.title) == 'string' &&
(caseSensitive ? value.title : value.title.toLowerCase())
.indexOf(query) >= 0) {
// Match found in title field
results.push(value);
}
else if ($.type(value.description) == 'string' &&
(caseSensitive ? value.description :
value.description.toLowerCase()).indexOf(query) >= 0) {
// Match found in description field
results.push(value);
}
break;
}
});
return results;
},
/**
* Fetch remote result data and return it using completeCallback when
* fetching is finished. Must be asynchronous in order to not freeze the
* Better Autocomplete instance.
*
* <br /><br /><em>Default behavior: Fetches JSON data from the url, using
* the jQuery.ajax() method. Errors are ignored.</em>
*
* @param {String} url
* The URL to fetch data from.
*
* @param {Function} completeCallback
* This function must be called, even if an error occurs. It takes zero
* or one parameter: the data that was fetched.
*
* @param {Number} timeout
* The preferred timeout for the request. This callback should respect
* the timeout.
*
* @param {Boolean} crossOrigin
* True if a cross origin request should be performed.
*/
fetchRemoteData: function(url, completeCallback, timeout, crossOrigin) {
$.ajax({
url: url,
dataType: crossOrigin && !$.support.cors ? 'jsonp' : 'json',
timeout: timeout,
success: function(data, textStatus) {
completeCallback(data);
},
error: function(jqXHR, textStatus, errorThrown) {
completeCallback();
}
});
},
/**
* Process remote fetched data by extracting an array of result objects
* from it. This callback is useful if the fetched data is not the plain
* results array, but a more complicated object which does contain results.
*
* <br /><br /><em>Default behavior: If the data is defined and is an
* array, return it. Otherwise return an empty array.</em>
*
* @param {mixed} data
* The raw data recieved from the server. Can be undefined.
*
* @returns {Array[Object]}
* A flat array containing result objects. May be an empty array.
*/
processRemoteData: function(data) {
if ($.isArray(data)) {
return data;
}
else {
return [];
}
},
/**
* From a given result object, return it's group name (if any). Used for
* grouping results together.
*
* <br /><br /><em>Default behavior: If the result has a "group" property
* defined, return it.</em>
*
* @param {Object} result
* The result object.
*
* @returns {String}
* The group name, may contain HTML. If no group, don't return anything.
*/
getGroup: function(result) {
if ($.type(result.group) == 'string') {
return result.group;
}
},
/**
* Called when remote fetching begins.
*
* <br /><br /><em>Default behavior: Adds the CSS class "fetching" to the
* input field, for styling purposes.</em>
*
* @param {Object} $input
* The input DOM element, wrapped in jQuery.
*/
beginFetching: function($input) {
$input.addClass('fetching');
},
/**
* Called when fetching is finished. All active requests must finish before
* this function is called.
*
* <br /><br /><em>Default behavior: Removes the "fetching" class.</em>
*
* @param {Object} $input
* The input DOM element, wrapped in jQuery.
*/
finishFetching: function($input) {
$input.removeClass('fetching');
},
/**
* Executed after the suggestion list has been shown.
*
* @param {Object} $results
* The suggestion list UL element, wrapped in jQuery.
*
* <br /><br /><em>Default behavior: Does nothing.</em>
*/
afterShow: function($results) {},
/**
* Executed after the suggestion list has been hidden.
*
* @param {Object} $results
* The suggestion list UL element, wrapped in jQuery.
*
* <br /><br /><em>Default behavior: Does nothing.</em>
*/
afterHide: function($results) {},
/**
* Construct the remote fetching URL.
*
* <br /><br /><em>Default behavior: Adds "?q=<query>" or "&q=<query>" to the
* path. The query string is URL encoded.</em>
*
* @param {String} path
* The path given in the {@link jQuery.betterAutocomplete} constructor.
*
* @param {String} query
* The raw query string. Remember to URL encode this to prevent illegal
* character errors.
*
* @returns {String}
* The URL, ready for fetching.
*/
constructURL: function(path, query) {
return path + (path.indexOf('?') > -1 ? '&' : '?') + 'q=' + encodeURIComponent(query);
},
/**
* To ease up on server load, treat similar strings the same.
*
* <br /><br /><em>Default behavior: Trims the query from leading and
* trailing whitespace.</em>
*
* @param {String} rawQuery
* The user's raw input.
*
* @param {Boolean} caseSensitive
* Case sensitive. Will convert to lowercase if false.
*
* @returns {String}
* The canonical query associated with this string.
*/
canonicalQuery: function(rawQuery, caseSensitive) {
var query = $.trim(rawQuery);
if (!caseSensitive) {
query = query.toLowerCase();
}
return query;
},
/**
* Insert the results list into the DOM and position it properly.
*
* <br /><br /><em>Default behavior: Inserts suggestion list directly
* after the input element and sets an absolute position using
* jQuery.position() for determining left/top values. Also adds a nice
* looking box-shadow to the list.</em>
*
* @param {Object} $results
* The UL list element to insert, wrapped in jQuery.
*
* @param {Object} $input
* The text input element, wrapped in jQuery.
*/
insertSuggestionList: function($results, $input) {
$results.width($input.outerWidth() - 2) // Subtract border width.
.css({
position: 'absolute',
zIndex: 10,
maxHeight: '330px',
// Visually indicate that results are in the topmost layer
boxShadow: '0 0 15px rgba(0, 0, 0, 0.5)'
})
.hide()
.insertAfter($input);
}
};
/*
* jQuery focus selector, required by Better Autocomplete.
*
* @see http://stackoverflow.com/questions/967096/using-jquery-to-test-if-an-input-has-focus/2684561#2684561
*/
var filters = $.expr[':'];
if (!filters.focus) {
filters.focus = function(elem) {
return elem === document.activeElement && (elem.type || elem.href);
};
}
})(jQuery);
@@ -0,0 +1,222 @@
/**
* @file
* Linkit styles.
*/
#linkit-modal {
overflow: visible;
padding: 20px;
}
#linkit-modal.progress-disabled {
padding: 0;
}
#linkit-modal .linkit-search-element,
#linkit-modal .linkit-path-element {
background-image: none;
font-size: 1em;
height: 2em;
padding: 3px 25px 3px 5px;
width: 440px;
}
#linkit-modal .linkit-search-element.fetching {
background-image: url(../throbber.gif);
background-position: 98% -8px;
background-repeat: no-repeat;
}
#linkit-modal .description,
#linkit-modal a.fieldset-title {
color: #999;
}
#linkit-modal .form-item input,
#linkit-modal .form-item select,
#linkit-modal fieldset.form-wrapper {
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
-moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15) inset;
-webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15) inset;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15) inset;
outline: none;
}
.ui-dialog.linkit-wrapper {
border: 1px solid #b2b2b2;
border-bottom-color: #999;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
-moz-box-shadow: 0 0 3px rgba(0, 0, 0, .15);
-webkit-box-shadow: 0 0 3px rgba(0, 0, 0, .15);
box-shadow: 0 0 3px rgba(0, 0, 0, .15);
overflow: visible;
padding: 0;
}
.ui-dialog.linkit-wrapper .ui-dialog-titlebar {
font-weight: bold;
font-size: 13px;
color: #474747;
text-shadow: 0 1px 0 rgba(255,255,255,.75);
border-bottom: 1px solid #999;
padding: 6px 10px;
-moz-border-radius: 2px 2px 0 0;
-webkit-border-radius: 2px 2px 0 0;
border-radius: 2px 2px 0 0;
-moz-box-shadow: 0 1px 0 #fff inset;
-webkit-box-shadow: 0 1px 0 #fff inset;
box-shadow: 0 1px 0 #fff inset;
background: #cfd1cf;
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#cfd1cf));
background-image: -moz-linear-gradient(top, #f5f5f5, #cfd1cf);
background-image: -webkit-linear-gradient(top, #f5f5f5, #cfd1cf);
background-image: -o-linear-gradient(top, #f5f5f5, #cfd1cf);
background-image: -ms-linear-gradient(top, #f5f5f5, #cfd1cf);
background-image: linear-gradient(top, #f5f5f5, #cfd1cf);
filter: progid:DXImageTransform.Microsoft.gradient(gradientType=0, startColorstr='#fff5f5f5', endColorstr='#ffcfd1cf');
}
#linkit-modal fieldset.form-wrapper {
width: 470px;
}
#linkit-modal .linkit-attributes .form-item {
overflow: hidden;
padding: 0;
}
#linkit-modal label {
color: #666;
font-size: 11px;
}
#linkit-modal .linkit-attributes .form-item label {
float: left;
margin-right: 1em;
width: 150px;
}
#linkit-modal .linkit-attributes .form-item input,
#linkit-modal .linkit-attributes .form-item select {
float: left;
padding: 5px;
width: 260px;
}
.linkit-wrapper > .ajax-progress {
padding:20px;
}
#linkit-profile-changer {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
border-top: 5px solid #eee;
left: 0;
position: absolute;
top: 0;
z-index: 211000;
width: 100%;
}
#linkit-profile-changer > div.form-item {
background-color: #EEE;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
color: #999;
display: none;
margin: 0;
padding: 10px 20px;
z-index: 1;
border-bottom: 1px solid #C9CCCF;
}
#linkit-profile-changer-toggler {
background-color: #EEE;
border: 1px solid #C9CCCF;
border-top: none;
-moz-border-radius-bottomleft: 3px;
-webkit-border-bottom-left-radius: 3px;
border-bottom-left-radius: 3px;
-webkit-border-bottom-right-radius: 3px;
-moz-border-radius-bottomright: 3px;
border-bottom-right-radius: 3px;
color: #999;
cursor:pointer;
float: right;
font-size: 10px;
font-weight: bold;
margin: -1px 30px 0 0;
text-align: center;
width: 100px;
}
#linkit-imce {
font-size: 10px;
}
.linkit-wrapper .ajax-progress .throbber {
background-image: url(../throbber.gif);
background-position: 98% -16px;
background-repeat: no-repeat;
height: 16px;
margin: 2px;
width: 16px;
}
/* Additional status types */
.better-autocomplete {
background-color: transparent;
border-bottom: none;
}
.better-autocomplete > .result.status-ok {
background: #397928;
border-top-color: #6ba15d;
}
.better-autocomplete > .result.status-warning {
background: #8a3131;
border-top-color: #b77878;
}
.better-autocomplete > .result.status-notice {
border-top-width: 0;
background: #575757;
}
.better-autocomplete > .result span.status {
font-style: italic;
}
.better-autocomplete > .result.unpublished-node {
background: #fff4f4;
}
.better-autocomplete > .result.unpublished-node.highlight {
background: #0075ba;
border-top-color: #1f4a64;
}
.better-autocomplete > .result img {
float: left;
margin: 0 5px 5px 0;
}
.better-autocomplete > .result > p:after {
clear: both;
content: ".";
display: block;
font-size: 0;
height: 0;
visibility: hidden;
}
.better-autocomplete li:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
border-bottom: 1px solid #BFBFBF;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

@@ -0,0 +1,53 @@
/**
* @file
* Linkit ckeditor dialog helper.
*/
(function ($) {
// Abort if Drupal.linkit is not defined.
if (typeof Drupal.linkit === 'undefined') {
return ;
}
Drupal.linkit.registerDialogHelper('ckeditor', {
init : function() {},
/**
* Prepare the dialog after init.
*/
afterInit : function () {
var editor = Drupal.settings.linkit.currentInstance.editor;
var element = CKEDITOR.plugins.link.getSelectedLink( editor );
// If we have selected a link element, lets populate the fields in the
// modal with the values from that link element.
if (element) {
link = {
path: element.data('cke-saved-href') || element.getAttribute('href') || '',
attributes: {}
},
// Get all attributes that have fields in the modal.
additionalAttributes = Drupal.linkit.additionalAttributes();
for (var i = 0; i < additionalAttributes.length; i++) {
link.attributes[additionalAttributes[i]] = element.getAttribute(additionalAttributes[i]);
}
// Populate the fields.
Drupal.linkit.populateFields(link);
}
},
/**
* Insert the link into the editor.
*
* @param {Object} link
* The link object.
*/
insertLink : function(link) {
var editor = Drupal.settings.linkit.currentInstance.editor;
CKEDITOR.tools.callFunction(editor._.linkitFnNum, link, editor);
}
});
})(jQuery);
@@ -0,0 +1,188 @@
/**
* @file
* Plugin for inserting links with Linkit.
*/
(function ($) {
CKEDITOR.plugins.add( 'linkit', {
requires : [ 'link' ],
hidpi: true,
icons: 'linkit',
init: function( editor ) {
// Get the major CKeditor verison.
// We do not care about minor versions.
var version = parseInt(CKEDITOR.version);
// Add Button.
editor.ui.addButton( 'linkit', {
label: Drupal.t('Link to content'),
command: 'linkit'
});
// Ckeditor version lower then 4 needs to have a icon path.
if (version < 4) {
editor.ui._.items.linkit.icon = this.path + 'icons/linkit.png';
editor.ui._.items.linkit.args[0].icon = this.path + 'icons/linkit.png';
}
// Add Command.
editor.addCommand( 'linkit', {
// FOR ACF in ckeditor 4.1+, allow everything.
allowedContent: 'a[*]{*}(*)',
exec : function () {
if (typeof Drupal.settings.linkit === 'undefined') {
alert(Drupal.t('Could not find the Linkit profile.'));
return ;
}
// Set the editor object.
Drupal.settings.linkit.currentInstance.editor = editor;
// Set profile.
Drupal.settings.linkit.currentInstance.profile = Drupal.settings.linkit.fields[editor.name].profile;
// Set the name of the source field.
Drupal.settings.linkit.currentInstance.source = editor.name;
// Set the source type.
Drupal.settings.linkit.currentInstance.helper = 'ckeditor';
var selection = editor.getSelection(),
element = null;
// If we have selected a link element, we what to grab its attributes
// so we can inserten them into the Linkit form in the dialog.
if ((element = CKEDITOR.plugins.link.getSelectedLink(editor)) && element.hasAttribute('href')) {
selection.selectElement(element);
}
else {
element = null;
}
// Save the selection.
Drupal.settings.linkit.currentInstance.selection = selection;
// Lock the selecton for IE.
if (CKEDITOR.env.ie && typeof selection !== 'undefined') {
selection.lock();
}
// Save the selected element.
Drupal.settings.linkit.currentInstance.selectedElement = element;
// Create the modal.
Drupal.linkit.createModal();
}
});
// If the "menu" plugin is loaded, register the menu items.
if (editor.addMenuItems) {
// Use the default link menu group weight and subtract one.
var defaultMenuGroup = editor._.menuGroups.link;
editor.addMenuGroup("Linkit", defaultMenuGroup - 1);
editor.addMenuItems({
linkit: {
label: Drupal.t('Link to content'),
command: 'linkit',
group: 'Linkit',
order: 0
}
});
// Remove the default link option.
editor.removeMenuItem('link');
}
// If the "contextmenu" plugin is loaded, register the listeners.
if (editor.contextMenu) {
editor.contextMenu.addListener(function(element, selection) {
if (!element || element.isReadOnly() || (selection.getSelectedText().length < 1 && !element.is('a'))) {
return null;
}
return {linkit: CKEDITOR.TRISTATE_OFF};
});
}
// Add a shortcut. Only CKeditor version 4 has this function.
if (version >= 4) {
editor.setKeystroke( CKEDITOR.CTRL + 76 /*L*/, 'linkit' );
}
// Add event listener.
editor.on( 'doubleclick', function( evt ) {
// Delete the default link dialog.
delete evt.data.dialog;
var element = CKEDITOR.plugins.link.getSelectedLink( editor ) || evt.data.element;
if ( !element.isReadOnly() ) {
if ( element.is( 'a' ) ) {
editor.getSelection().selectElement( element );
if (version >= 4) {
editor.commands.linkit.exec();
}
else if(version == 3) {
editor._.commands.linkit.exec();
}
}
}
});
// Register an extra fucntion, this will be used in the modal.
editor._.linkitFnNum = CKEDITOR.tools.addFunction( insertLink, editor );
}
});
/**
* Create or update a link element in the editor.
*/
function insertLink(data, editor) {
var selection = editor.getSelection();
data.path = CKEDITOR.tools.trim(data.path);
// Browser need the "href" for copy/paste link to work. (CKEDITOR ISSUE #6641)
data.attributes['data-cke-saved-href'] = data.path;
if (!Drupal.settings.linkit.currentInstance.selectedElement) {
// We have not selected any link element so lets create a new one.
var range = selection.getRanges(1)[0];
if (range.collapsed) {
var content = (Drupal.settings.linkit.currentInstance.linkContent) ? Drupal.settings.linkit.currentInstance.linkContent : data.path;
var text = new CKEDITOR.dom.text(content , editor.document );
range.insertNode(text);
range.selectNodeContents(text);
}
// Delete all attributes that are empty.
data.attributes.href = data.path;
for (name in data.attributes) {
data.attributes[name] ? null : delete data.attributes[name];
}
// Apply style.
var style = new CKEDITOR.style({element : 'a', attributes : data.attributes});
style.type = CKEDITOR.STYLE_INLINE;
style.applyToRange(range);
range.select();
}
else {
var element = Drupal.settings.linkit.currentInstance.selectedElement;
// We are editing an existing link, so just overwrite the attributes.
element.setAttribute('href', data.path);
element.setAttribute('data-cke-saved-href', data.path);
for (name in data.attributes) {
data.attributes[name] ?
element.setAttribute(name, data.attributes[name]) :
element.removeAttribute(name);
}
selection.selectElement( element );
}
// Unlock the selection.
if (CKEDITOR.env.ie && typeof selection !== 'undefined') {
selection.unlock();
}
}
})(jQuery);
@@ -0,0 +1,78 @@
/**
* @file
* Plugin for inserting links with Linkit.
*/
(function ($) {
tinymce.create('tinymce.plugins.linkit', {
init : function(editor, url) {
// Register commands
editor.addCommand('mceLinkit', function() {
if (typeof Drupal.settings.linkit === 'undefined') {
alert(Drupal.t('Could not find the Linkit profile.'));
return ;
}
// Set the editor object.
Drupal.settings.linkit.currentInstance.editor = editor;
// Set profile.
Drupal.settings.linkit.currentInstance.profile = Drupal.settings.linkit.fields[editor.id].profile;
// Set the name of the source field..
Drupal.settings.linkit.currentInstance.source = editor.id;
// Set the source type.
Drupal.settings.linkit.currentInstance.helper = 'tinymce';
// Stores the current editor selection for later restoration. This can
// be useful since some browsers looses it's selection if a control
// element is selected/focused inside the dialogs.
editor.windowManager.bookmark = editor.selection.getBookmark(1);
// Create the modal.
Drupal.linkit.createModal();
});
// Register buttons
editor.addButton('linkit', {
title : Drupal.t('Link to content'),
cmd : 'mceLinkit',
image : url + '/images/linkit.png'
});
// We need the real contextmenu in order to make this work.
if (editor && editor.plugins.contextmenu) {
// Contextmenu gets called - this is what we do.
editor.plugins.contextmenu.onContextMenu.add(function(th, m, e, col) {
// Only if selected node is an link do this.
if (e.nodeName == 'A' || !col) {
// Remove all options from standard contextmenu.
m.removeAll();
th._menu.add({
title : Drupal.t('Link to content'),
cmd : 'mceLinkit',
icon : 'linkit'
});
//m.addSeparator();
}
});
}
},
getInfo : function() {
return {
longname : 'Linkit',
author : 'Emil Stjerneman',
authorurl : 'http://www.stjerneman.com',
infourl : 'http://drupal.org/project/linkit',
version : tinymce.majorVersion + "." + tinymce.minorVersion
};
}
});
// Register plugin
tinymce.PluginManager.add('linkit', tinymce.plugins.linkit);
})(jQuery);
Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

@@ -0,0 +1,102 @@
/**
* @file
* Linkit tinymce dialog helper.
*/
(function ($) {
// Abort if Drupal.linkit is not defined.
if (typeof Drupal.linkit === 'undefined') {
return ;
}
Drupal.linkit.registerDialogHelper('tinymce', {
init : function() {},
/**
* Prepare the dialog after init.
*/
afterInit : function () {
var editor = Drupal.settings.linkit.currentInstance.editor;
var element, link;
// Restore the selection if the browser is IE.
if (tinymce.isIE) {
editor.selection.moveToBookmark(editor.windowManager.bookmark);
}
// If we have selected a link element, lets populate the fields in the
// dialog with the values from that link element.
if (element = editor.dom.getParent(editor.selection.getNode(), 'A')) {
link = {
path: editor.dom.getAttrib(element, 'href'),
attributes: {}
};
// Get all attributes that have fields in the modal.
var additionalAttributes = Drupal.linkit.additionalAttributes();
// Add attributes to the link object, but only those that are enabled in Linkit.
tinymce.each(additionalAttributes, function(attribute) {
var value = editor.dom.getAttrib(element, attribute);
if (value) {
link.attributes[attribute] = value;
}
});
}
// Populate the fields.
Drupal.linkit.populateFields(link);
},
/**
* Insert the link into the editor.
*
* @param {Object} link
* The link object.
*/
insertLink : function(data) {
var editor = Drupal.settings.linkit.currentInstance.editor,
element = editor.dom.getParent(editor.selection.getNode(), 'A');
// Restore the selection if the browser is IE.
if (tinymce.isIE) {
editor.selection.moveToBookmark(editor.windowManager.bookmark);
}
// Set undo begin point.
editor.execCommand("mceBeginUndoLevel");
data.attributes.href = data.path;
// No link element selected, create a new anchor element.
if (element == null) {
// If there is no selection, lets inser a new element.
if (editor.selection.isCollapsed()) {
var content = (Drupal.settings.linkit.currentInstance.linkContent) ? Drupal.settings.linkit.currentInstance.linkContent : data.path;
editor.execCommand('mceInsertContent', false,
editor.dom.createHTML('a', data.attributes, content));
} else {
editor.execCommand("mceInsertLink", false, data.attributes);
}
}
// We are editing an existing link, so just overwrite the attributes.
else {
editor.dom.setAttribs(element, data.attributes);
}
// Don't move caret if selection was image
if(element != null) {
if (element.childNodes.length != 1 || element.firstChild.nodeName != 'IMG') {
editor.focus();
editor.selection.select(element);
editor.selection.collapse(0);
// Restore the selection if the browser is IE.
if (tinymce.isIE) {
editor.selection.moveToBookmark(editor.windowManager.bookmark);
}
}
}
// Set undo end point.
editor.execCommand("mceEndUndoLevel");
}
});
})(jQuery);
@@ -0,0 +1,102 @@
<?php
/**
* @file
* Linkit Profile class.
*/
/**
* Linkit Profile class implementation.
*/
class LinkitProfile {
/**
* The profile data (settings).
*
* @var array
*/
public $data;
/**
* All enabled attributes for this profile.
*
* @var array
*/
protected $enabled_attribute_plugins;
/**
* All enabled search plugins for this profile.
*
* @var array
*/
protected $enabled_search_plugins;
/**
* Set all enabled attribure plugins.
*/
public function setEnabledAttributePlugins() {
foreach ($this->data['attribute_plugins'] as $attribute_name => $attribute) {
if ($attribute['enabled']) {
// Load the attribute plugin.
$attribute_plugin = linkit_attribute_plugin_load($attribute_name);
// Call the callback to get the FAPI element.
if (isset($attribute_plugin['callback']) && function_exists($attribute_plugin['callback'])) {
$attribute_html = $attribute_plugin['callback']($attribute_plugin, $attribute);
// Add Linkit specific class, this is used by the editor JS scripts.
$attribute_html['#attributes']['class'][] = 'linkit-attribute';
$this->enabled_attribute_plugins[$attribute_name] = $attribute_html;
}
}
}
}
/**
* Set all enabled search plugins.
*/
public function setEnabledsearchPlugins() {
// Sort plugins by weight.
uasort($this->data['search_plugins'], 'linkit_sort_plugins_by_weight');
foreach ($this->data['search_plugins'] as $plugin_name => $plugin) {
if ($plugin['enabled']) {
// Load plugin definition.
$plugin_definition = linkit_search_plugin_load($plugin_name);
// Get a Linkit search plugin object.
$search_plugin = LinkitSearchPlugin::factory($plugin_definition, $this);
// Only register none broken plugins.
if ($search_plugin->broken() !== TRUE) {
$this->enabled_search_plugins[$plugin_name] = $search_plugin;
}
}
}
}
/**
* Construct an array with all the enabled attribute plugins for this profile.
*
* @return
* An array with all enabled attribute plugins for this profile.
*/
public function getEnabledAttributePlugins() {
if (!isset($this->enabled_attribute_plugins)) {
$this->setEnabledAttributePlugins();
}
return $this->enabled_attribute_plugins;
}
/**
* Construct an array with all enabled search plugins for this profile.
*
* @return
* An array with all enabled search plugins for this profile.
*/
public function getEnabledsearchPlugins() {
if (!isset($this->enabled_search_plugins)) {
$this->setEnabledsearchPlugins();
}
return $this->enabled_search_plugins;
}
}
@@ -0,0 +1,171 @@
<?php
/**
* @file
* Linkit Search plugin interface.
*
* Provides an interface and classes to implement Linkit search plugins.
*/
/**
* Defines a common interface for a Linkit search plugin.
*/
interface LinkitSearchPluginInterface {
/**
* Search plugin factory method.
*
* @param $plugin
* A search plugin object.
*
* @param LinkitProfile $profile
* (optional) A LinkitProfile object.
*
* @return
* An instance of the search plugin class or an instance of the
* LinkitSearchPluginBroken class.
*/
public static function factory($plugin, LinkitProfile $profile);
/**
* Return a string representing this handler's name in the UI.
*/
public function ui_title();
/**
* Return a string representing this handler's description in the UI.
*/
public function ui_description();
/**
* Fetch search results based on the $search_string.
*
* @param $search_string
* A string that contains the text to search for.
*
* @return
* An associative array whose values are an
* associative array containing:
* - title: A string to use as the search result label.
* - description: (optional) A string with additional information about the
* result item.
* - path: The URL to the item.
* - group: (optional) A string with the group name for the result item.
* Best practice is to use the plugin name as group name.
* - addClass: (optional) A string with classes to add to the result row..
*/
public function fetchResults($search_string);
}
/**
* Base class for Linkit search plugins.
*/
abstract class LinkitSearchPlugin implements LinkitSearchPluginInterface {
/**
* The plugin definition for this instance.
*
* @var array
*/
protected $plugin;
/**
* The profile instance for this instance.
*
* @var LinkitProfile object
*/
protected $profile;
/**
* Initialize this search plugin with the search plugin and the profile.
*
* @param $plugin
* A search plugin object.
*
* @param LinkitProfile $profile
* A LinkitProfile object.
*/
public function __construct($plugin, LinkitProfile $profile) {
$this->plugin = $plugin;
$this->profile = $profile;
}
/**
* Implements LinkitSearchPluginInterface::factory().
*/
public static function factory($plugin, LinkitProfile $profile) {
ctools_include('plugins');
// Make sure that the handler class exists and that it has this class as one
// of its parents.
if (class_exists($plugin['handler']['class']) && is_subclass_of($plugin['handler']['class'], __CLASS__)) {
return new $plugin['handler']['class']($plugin, $profile);
}
else {
// The plugin handler class is defined but it cannot be found, so lets
// instantiate the LinkitSearchPluginBroken instead.
return new LinkitSearchPluginBroken($plugin, $profile);
}
}
/**
* Implements LinkitSearchPluginInterface::ui_title().
*/
public function ui_title() {
if (!isset($this->plugin['ui_title'])) {
return check_plain($this->plugin['module'] . ':' . $this->plugin['name']);
}
return check_plain($this->plugin['ui_title']);
}
/**
* Implements LinkitSearchPluginInterface::ui_description().
*/
public function ui_description() {
if (isset($this->plugin['ui_description'])) {
return check_plain($this->plugin['ui_description']);
}
}
/**
* Generate a settings form for this handler.
* Uses the standard Drupal FAPI.
*
* @return
* An array containing any custom form elements to be displayed in the
* profile editing form
*/
public function buildSettingsForm() {}
/**
* Determine if the handler is considered 'broken', meaning it's a
* a placeholder used when a handler can't be found.
*/
public function broken() { }
}
/**
* A special handler to take the place of missing or broken Linkit search
* plugin handlers.
*/
class LinkitSearchPluginBroken extends LinkitSearchPlugin {
/**
* Overrides LinkitSearchPlugin::ui_title().
*/
public function ui_title() { return t('Broken/missing handler'); }
/**
* Overrides LinkitSearchPlugin::ui_description().
*/
public function ui_description() {}
/**
* Implements LinkitSearchPluginInterface::fetchResults().
*/
public function fetchResults($search_string) {}
/**
* Overrides LinkitSearchPlugin::broken().
*/
public function broken() { return TRUE; }
}
@@ -0,0 +1,46 @@
<?php
/**
* @file
* Linkit theme functions.
*/
/**
* Theme callback for the search/attribute plugins in linkit_profiles_form.
*/
function theme_linkit_plugin_form_table($variables) {
$form = $variables['form'];
$has_description = FALSE;
$rows = array();
// Iterate over each element.
foreach (element_children($form) as $id) {
$form[$id]['weight']['#attributes']['class'] = array('weight');
$fields = array(
drupal_render($form[$id]['name']),
drupal_render($form[$id]['weight']),
drupal_render($form[$id]['enabled']),
);
if (isset($form[$id]['description'])) {
$has_description = TRUE;
$fields[] = drupal_render($form[$id]['description']);
}
$rows[$id]['data'] = $fields;
$rows[$id]['class'] = array('draggable');
}
drupal_add_tabledrag('linkit-search-plugins', 'order', 'sibling', 'weight');
$header = array(
t('Name'),
t('Weight'),
t('Enabled'),
);
if ($has_description) {
$header[] = t('Description');
}
return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'linkit-search-plugins'), 'sticky' => FALSE));
}
@@ -0,0 +1,243 @@
/**
* @file
* Linkit dashboard functions
*/
(function ($) {
Drupal.behaviors.linkitDashboard = {
attach: function (context, settings) {
// Bind the insert link button.
$('.linkit-insert', context).once('linkit-insert', function() {
$('.linkit-insert', context).click(function() {
// Call the insertLink() function.
Drupal.linkit.getDialogHelper(Drupal.settings.linkit.currentInstance.helper).insertLink(Drupal.linkit.getLink());
// Close the dialog.
Drupal.linkit.modalClose();
return false;
});
});
// Bind the close link.
$('#linkit-cancel', context).once('linkit-cancel', function() {
$('#linkit-cancel', context).bind('click', Drupal.linkit.modalClose);
});
// Run the validation if the path field is populated directly.
$('#edit-linkit-path', context).bind('keyup paste input propertychange', function(){
Drupal.linkit.requiredFieldsValidation();
});
$(".ui-dialog-titlebar").show();
// Run required field validation.
Drupal.linkit.requiredFieldsValidation();
if (!Drupal.settings.linkit.currentInstance.suppressProfileChanger) {
// Make the profile changer
Drupal.linkit.profileChanger(context);
}
if (Drupal.settings.linkit.IMCEurl && !$('#linkit-imce', context).length) {
var $imceButton = $('<input />')
.attr({type: 'button', id: 'linkit-imce', name: 'linkit-imce'})
.addClass('form-submit')
.val(Drupal.t('Open file browser'))
.insertAfter($('.form-item-linkit-search', context))
.click(function(e) {
e.preventDefault();
Drupal.linkit.openFileBrowser();
});
}
}
};
/**
* Check for mandatory fields in the form and disable for submissions
* if any of the fields are empty.
*/
Drupal.linkit.requiredFieldsValidation = function() {
var allowed = true;
$('#linkit-modal .required').each(function() {
if (!$(this).val()) {
allowed = false;
return false;
}
});
if (allowed) {
$('#linkit-modal .linkit-insert')
.removeAttr('disabled')
.removeClass('form-button-disabled');
}
else {
$('#linkit-modal .linkit-insert')
.attr('disabled', 'disabled')
.addClass('form-button-disabled');
}
};
/**
* Open the IMCE file browser
*/
Drupal.linkit.openFileBrowser = function () {
window.open(decodeURIComponent(Drupal.settings.linkit.IMCEurl), '', 'width=760,height=560,resizable=1');
};
/**
* When a file is inserted through IMCE, this function is called.
* See IMCE api for details.
*
* @param file
* The file object that was selected inside IMCE
* @param win
* The IMCE window object
*/
Drupal.linkit.IMCECallback = function(file, win) {
Drupal.linkit.populateFields({
path: win.imce.decode(Drupal.settings.basePath +
Drupal.settings.linkit.publicFilesDirectory + '/' + file.relpath)
});
win.close();
};
/**
* Populate fields on the dashboard.
*
* @param link
* An object with the following properties (all are optional):
* - path: The anchor's href.
* - attributes: An object with additional attributes for the anchor element.
*/
Drupal.linkit.populateFields = function(link) {
link = link || {};
link.attributes = link.attributes || {};
$('#linkit-modal .linkit-path-element').val(link.path);
$.each(link.attributes, function(name, value) {
$('#linkit-modal .linkit-attributes .linkit-attribute-' + name).val(value);
});
// Run required field validation.
Drupal.linkit.requiredFieldsValidation();
};
/**
* Retrieve a link object by extracting values from the form.
*
* @return
* The link object.
*/
Drupal.linkit.getLink = function() {
var link = {
path: $('#linkit-modal .linkit-path-element ').val(),
attributes: {}
};
$.each(Drupal.linkit.additionalAttributes(), function(f, name) {
link.attributes[name] = $('#linkit-modal .linkit-attributes .linkit-attribute-' + name).val();
});
return link;
};
/**
* Retrieve a list of the currently available additional attributes in the
* dashboard. The attribute "href" is excluded.
*
* @return
* An array with the names of the attributes.
*/
Drupal.linkit.additionalAttributes = function() {
var attributes = [];
$('#linkit-modal .linkit-attributes .linkit-attribute').each(function() {
// Remove the 'linkit_' prefix.
attributes.push($(this).attr('name').substr(7));
});
return attributes;
};
Drupal.linkit.profileChanger = function(context) {
$('#linkit-profile-changer > div.form-item', context).once('linkit-change-profile', function() {
var target = $(this);
var toggler = $('<div id="linkit-profile-changer-toggler"></div>')
.html(Drupal.t('Change profile'))
.click(function() {
target.slideToggle();
});
$(this).after(toggler);
});
$('#linkit-profile-changer .form-radio', context).each(function() {
var id = $(this).attr('id');
var profile = $(this).val();
if (typeof Drupal.ajax[id] != 'undefined') {
// @TODO: Jquery 1.5 accept success setting to be an array of functions.
// But we have to wait for jquery to get updated in Drupal core.
// In the meantime we have to override it.
Drupal.ajax[id].options.success = function (response, status) {
if (typeof response == 'string') {
response = $.parseJSON(response);
}
// Update the autocomplete url.
Drupal.settings.linkit.currentInstance.autocompletePathParsed = Drupal.settings.linkit.autocompletePath.replace('___profile___', profile);
// Call the ajax success method.
Drupal.ajax[id].success(response, status);
$('#linkit-profile-changer > div.form-item').slideToggle();
};
}
});
};
Drupal.behaviors.linkitSearch = {
attach: function(context, settings) {
$('.linkit-search-element').once('linkit-search', function() {
// Create a synonym for this to reduce code confusion.
var searchElement = $('.linkit-search-element');
var callbacks = {
constructURL: function(path, search) {
return path + encodeURIComponent(search);
},
insertSuggestionList: function($results, $input) {
var top = $input.position().top + $input.outerHeight() - 5;
$results.width($input.outerWidth()).css({
position: 'absolute',
// High value because of other overlays like
// wysiwyg fullscreen (TinyMCE) mode.
zIndex: 211000,
maxHeight: $(window).height() - (top + 20)
})
.hide()
.insertAfter($input);
},
select: function(result) {
if (typeof result == 'undefined') {
return false;
}
// Only change the link text if it is empty.
if (typeof result.disabled != 'undefined' && result.disabled) {
return false;
}
Drupal.linkit.populateFields({
path: result.path
});
// Store the result title (Used when no selection is made by the user).
Drupal.settings.linkit.currentInstance.linkContent = result.title;
$('.linkit-path-element', context).focus();
}
}
searchElement.betterAutocomplete('init', Drupal.settings.linkit.currentInstance.autocompletePathParsed, Drupal.settings.linkit.currentInstance.autocomplete, callbacks);
});
}
};
})(jQuery);
@@ -0,0 +1,135 @@
/**
* @file
* Linkit field ui functions
*/
(function ($) {
Drupal.behaviors.linkit_field = {
attach : function(context, settings) {
// If there is no fields, just stop here.
if (settings.linkit == undefined || settings.linkit.fields == null) {
return false;
}
$.each(settings.linkit.fields, function(field_name, field) {
$('#' + field_name, context).once('linkit_field', function() {
$('.linkit-field-' + field_name).click(function() {
// Set profile.
Drupal.settings.linkit.currentInstance.profile = Drupal.settings.linkit.fields[field_name].profile;
// Set the name of the source field..
Drupal.settings.linkit.currentInstance.source = field_name;
// Set the source type.
Drupal.settings.linkit.currentInstance.helper = 'field';
// Only care about selection if the element is a textarea.
if ($('textarea#' + field_name).length) {
var selection = Drupal.linkit.getDialogHelper('field').getSelection($('#' + field_name).get(0));
// Save the selection.
Drupal.settings.linkit.currentInstance.selection = selection;
}
// Suppress profile changer.
Drupal.settings.linkit.currentInstance.suppressProfileChanger = true;
// Create the modal.
Drupal.linkit.createModal();
return false;
});
});
});
}
};
/**
* Linkit field dialog helper.
*/
Drupal.linkit.registerDialogHelper('field', {
init : function() {},
afterInit : function () {},
/**
* Insert the link into the field.
*
* @param {Object} link
* The link object.
*/
insertLink : function(data) {
var source = $('#' + Drupal.settings.linkit.currentInstance.source),
field_settings = Drupal.settings.linkit.fields[Drupal.settings.linkit.currentInstance.source],
// Call the insert plugin.
link = Drupal.linkit.getInsertPlugin(field_settings.insert_plugin).insert(data, field_settings);
if (typeof Drupal.settings.linkit.currentInstance.selection != 'undefined') {
// Replace the selection and insert the link there.
this.replaceSelection(source.get(0), Drupal.settings.linkit.currentInstance.selection, link);
}
else {
// Replace the field value.
this.replaceFieldValue(source.get(0), link);
}
// Link field can have a title field. If they have, we populate the title
// field with the search result title if any.
if (typeof field_settings.title_field != 'undefined' && typeof Drupal.settings.linkit.currentInstance.linkContent != 'undefined') {
this.replaceFieldValue($('#' + field_settings.title_field).get(0), Drupal.settings.linkit.currentInstance.linkContent);
}
},
/**
* Get field selection.
*/
getSelection : function(e) {
// Mozilla and DOM 3.0.
if ('selectionStart' in e) {
var l = e.selectionEnd - e.selectionStart;
return { start: e.selectionStart, end: e.selectionEnd, length: l, text: e.value.substr(e.selectionStart, l) };
}
// IE.
else if(document.selection) {
e.focus();
var r = document.selection.createRange(),
tr = e.createTextRange(),
tr2 = tr.duplicate();
tr2.moveToBookmark(r.getBookmark());
tr.setEndPoint('EndToStart',tr2);
if (r == null || tr == null) {
return { start: e.value.length, end: e.value.length, length: 0, text: '' };
}
// For some reason IE doesn't always count the \n and \r in the length.
var text_part = r.text.replace(/[\r\n]/g,'.'),
text_whole = e.value.replace(/[\r\n]/g,'.'),
the_start = text_whole.indexOf(text_part, tr.text.length);
return { start: the_start, end: the_start + text_part.length, length: text_part.length, text: r.text };
}
// Browser not supported.
else {
return { start: e.value.length, end: e.value.length, length: 0, text: '' };
}
},
/**
* Replace the field selection.
*/
replaceSelection : function (e, selection, text) {
var start_pos = selection.start;
var end_pos = start_pos + text.length;
e.value = e.value.substr(0, start_pos) + text + e.value.substr(selection.end, e.value.length);
},
/**
* Replace the field value.
*/
replaceFieldValue : function (e, text) {
e.value = text;
}
});
})(jQuery);
@@ -0,0 +1,162 @@
/**
* @file
* Linkit dialog functions.
*/
(function ($) {
// Create the Linkit namespaces.
Drupal.linkit = Drupal.linkit || {};
Drupal.linkit.currentInstance = Drupal.linkit.currentInstance || {};
Drupal.linkit.dialogHelper = Drupal.linkit.dialogHelper || {};
Drupal.linkit.insertPlugins = Drupal.linkit.insertPlugins || {};
/**
* Create the modal dialog.
*/
Drupal.linkit.createModal = function() {
// Create the modal dialog element.
Drupal.linkit.createModalElement()
// Create jQuery UI Dialog.
.dialog(Drupal.linkit.modalOptions())
// Remove the title bar from the modal.
.siblings(".ui-dialog-titlebar").hide();
// Make the modal seem "fixed".
$(window).bind("scroll resize", function() {
$('#linkit-modal').dialog('option', 'position', ['center', 50]);
});
// Get modal content.
Drupal.linkit.getDashboard();
};
/**
* Create and append the modal element.
*/
Drupal.linkit.createModalElement = function() {
// Create a new div and give it an ID of linkit-modal.
// This is the dashboard container.
var linkitModal = $('<div id="linkit-modal"></div>');
// Create a modal div in the <body>.
$('body').append(linkitModal);
return linkitModal;
};
/**
* Default jQuery dialog options used when creating the Linkit modal.
*/
Drupal.linkit.modalOptions = function() {
return {
dialogClass: 'linkit-wrapper',
modal: true,
draggable: false,
resizable: false,
width: 520,
position: ['center', 50],
minHeight: 0,
zIndex: 210000,
close: Drupal.linkit.modalClose,
open: function (event, ui) {
// Change the overlay style.
$('.ui-widget-overlay').css({
opacity: 0.5,
filter: 'Alpha(Opacity=50)',
backgroundColor: '#FFFFFF'
});
},
title: 'Linkit'
};
};
/**
* Close the Linkit modal.
*/
Drupal.linkit.modalClose = function (e) {
$('#linkit-modal').dialog('destroy').remove();
// Make sure the current intstance settings are removed when the modal is
// closed.
Drupal.settings.linkit.currentInstance = {};
// The event object does not have a preventDefault member in
// Internet Explorer prior to version 9.
if (e && e.preventDefault) {
e.preventDefault();
}
else {
return false;
}
};
/**
*
*/
Drupal.linkit.getDashboard = function () {
// Create the AJAX object.
var ajax_settings = {};
ajax_settings.event = 'LinkitDashboard';
ajax_settings.url = Drupal.settings.linkit.dashboardPath + Drupal.settings.linkit.currentInstance.profile;
ajax_settings.progress = {
type: 'throbber',
message : Drupal.t('Loading Linkit dashboard...')
};
Drupal.ajax['linkit-modal'] = new Drupal.ajax('linkit-modal', $('#linkit-modal')[0], ajax_settings);
// @TODO: Jquery 1.5 accept success setting to be an array of functions.
// But we have to wait for jquery to get updated in Drupal core.
// In the meantime we have to override it.
Drupal.ajax['linkit-modal'].options.success = function (response, status) {
if (typeof response == 'string') {
response = $.parseJSON(response);
}
// Call the ajax success method.
Drupal.ajax['linkit-modal'].success(response, status);
// Run the afterInit function.
var helper = Drupal.settings.linkit.currentInstance.helper;
Drupal.linkit.getDialogHelper(helper).afterInit();
// Set focus in the search field.
$('#linkit-modal .linkit-search-element').focus();
};
// Update the autocomplete url.
Drupal.settings.linkit.currentInstance.autocompletePathParsed =
Drupal.settings.linkit.autocompletePath.replace('___profile___', Drupal.settings.linkit.currentInstance.profile);
// Trigger the ajax event.
$('#linkit-modal').trigger('LinkitDashboard');
};
/**
* Register new dialog helper.
*/
Drupal.linkit.registerDialogHelper = function(name, helper) {
Drupal.linkit.dialogHelper[name] = helper;
};
/**
* Get a dialog helper.
*/
Drupal.linkit.getDialogHelper = function(name) {
return Drupal.linkit.dialogHelper[name];
};
/**
* Register new insert plugins.
*/
Drupal.linkit.registerInsertPlugin = function(name, plugin) {
Drupal.linkit.insertPlugins[name] = plugin;
};
/**
* Get an insert plugin.
*/
Drupal.linkit.getInsertPlugin = function(name) {
return Drupal.linkit.insertPlugins[name];
};
})(jQuery);
@@ -0,0 +1,32 @@
<?php
/**
* Implements hook_linkit_plugin_entities_alter().
*
* The default behavior for entities is that they will use the default entity
* search plugin class, which will provide them with the basic methods they
* need.
*
* Tho there can be some search plugins that will extend those basic method with
* more advanced once, therefore the handlers for those plugins will have to be
* changed.
*
* Make sure that your classes is included in the regisrty.
* The easiest way to do this is by define them as
*
* <code> files[] = plugins/linkit_search/my_custom_plugin.class.php </code>
*
* @param $plugins
* An array of all search plugins processed within Linkit entity plugin.
*/
function hook_linkit_search_plugin_entities_alter(&$plugins) {
$path = drupal_get_path('module', 'mymodule') . '/plugins/linkit_search';
if (isset($plugins['my_custom_plugin'])) {
$handler = array(
'class' => 'MyCustomPlugin',
'file' => 'my_custom_plugin.class.php',
'path' => $path,
);
$plugins['my_custom_plugin']['handler'] = $handler;
}
}
@@ -0,0 +1,241 @@
<?php
/**
* @file
* Implementation for Fields and Linkit.
*/
/**
* Implements hook_linkit_allowed_field_types().
*/
function linkit_linkit_allowed_field_types() {
$allowed_field_types = array(
'text',
'text_long',
'text_with_summary',
'link_field',
);
return $allowed_field_types;
}
/**
* Get all allowed filed types.
* @return
* An array of allowed filed types.
*/
function linkit_get_allowed_field_types() {
// Get allowed field and widget types.
$allowed_field_types = module_invoke_all('linkit_allowed_field_types');
drupal_alter('linkit_allowed_field_types', $allowed_field_types);
return $allowed_field_types;
}
/**
* Implements hook_linkit_allowed_elements().
*/
function linkit_linkit_allowed_field_elements() {
$allowed_elements = array(
'textfield',
'textarea',
'textarea_with_summary',
'link_field',
);
return $allowed_elements;
}
/**
* Get all allowed filed elements.
* @return
* An array of allowed filed elements.
*/
function linkit_get_allowed_field_elements() {
// Get allowed field and widget types.
$allowed_field_elements = module_invoke_all('linkit_allowed_field_elements');
drupal_alter('linkit_allowed_field_elements', $allowed_field_elements);
return $allowed_field_elements;
}
/**
* Implements hook_linkit_allowed_field_widget_types().
*/
function linkit_linkit_allowed_field_widget_types() {
$allowed_field_widget_types = array(
'text_textfield',
'text_textarea',
'text_textarea_with_summary',
'link_field',
);
return $allowed_field_widget_types;
}
/**
* Get all allowed filed widget types.
* @return
* An array of allowed filed widget types.
*/
function linkit_get_allowed_field_widget_types() {
// Get allowed field and widget types.
$allowed_field_widget_types = module_invoke_all('linkit_allowed_field_widget_types');
drupal_alter('linkit_allowed_field_widget_types', $allowed_field_widget_types);
return $allowed_field_widget_types;
}
/**
* Implements hook_form_FIELD_UI_FIELD_EDIT_FORM_alter().
*/
function linkit_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
$instance = $form['#instance'];
if (isset($form['locked']) && $form['locked']) {
return;
}
// Get allowed field types.
$allowed_field_types = linkit_get_allowed_field_types();
// Get allowed field widget types.
$allowed_field_widget_types = linkit_get_allowed_field_widget_types();
$allowed_field = in_array($form['#field']['type'], $allowed_field_types);
$allowed_widget = in_array($form['instance']['widget']['type']['#value'], $allowed_field_widget_types);
// Add the linkit settings to the field instance form if it's valid.
if ($allowed_field && $allowed_widget) {
// Fieldset for Linkit settings on this field instance.
$form['instance']['settings']['linkit'] = array(
'#type' => 'fieldset',
'#title' => t('Linkit field settings'),
'#collapsible' => FALSE,
'#collapsed' => FALSE,
);
// Enable Linkit on this field instance.
$form['instance']['settings']['linkit']['enable'] = array(
'#type' => 'checkbox',
'#title' => t('Enable Linkit for this field.'),
'#default_value' => isset($instance['settings']['linkit']['enable'])
? $instance['settings']['linkit']['enable'] : 0,
'#description' => t('Do not use this for CKeditor and TinyMCE fields. You will have to configure this on the wysiwyg/ckeditor profile.'),
);
$profiles = linkit_profile_field_load_all();
$options = array();
foreach ($profiles as $profile) {
$options[$profile->name] = $profile->admin_title;
}
// Sort the options.
natsort($options);
$form['instance']['settings']['linkit']['profile'] = array(
'#type' => 'select',
'#title' => t('Profile'),
'#options' => $options,
'#empty_option' => t('- Select a profile -'),
'#default_value' => isset($instance['settings']['linkit']['profile'])
? $instance['settings']['linkit']['profile'] : '',
'#states' => array(
'invisible' => array(
'input[name="instance[settings][linkit][enable]"]' => array('checked' => FALSE),
),
'required' => array(
'input[name="instance[settings][linkit][enable]"]' => array('checked' => TRUE),
),
),
'#element_validate' => array('linkit_field_profile_validate'),
);
// Enable Linkit on this field instance.
$form['instance']['settings']['linkit']['button_text'] = array(
'#type' => 'textfield',
'#title' => t('Button text that activates linkit modal.'),
'#default_value' => isset($instance['settings']['linkit']['button_text'])
? $instance['settings']['linkit']['button_text'] : t('Search'),
);
}
}
/**
* Validation callback; Only validate the profile field if linkit is enabled on
* the instance.
*
* @see linkit_form_field_ui_field_edit_form_alter()
*/
function linkit_field_profile_validate($element, &$form_state, $form) {
if (isset($form_state['values']['instance']['settings']['linkit']['enable'])
&& $form_state['values']['instance']['settings']['linkit']['enable']) {
if (empty($element['#value'])) {
form_error($element, t('You must select an profile.'));
}
}
}
/**
* Process callback.
*/
function linkit_process_field_element($element, &$form_state, &$complete_form) {
// Only proceed if the field is attached to an entity.
if (!isset($element['#entity_type'])) {
return $element;
}
$field = field_info_field($element['#field_name']);
$instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
if (isset($instance['settings']['linkit']['enable']) && $instance['settings']['linkit']['enable']) {
// Load the profile.
$profile = linkit_profile_load($instance['settings']['linkit']['profile']);
if (!$profile) {
return $element;
}
// Load the insert plugin for the profile.
$insert_plugin = linkit_insert_plugin_load($profile->data['insert_plugin']['plugin']);
if ($insert_plugin === NULL) {
return $element;
}
// Set the field ID.
$field_id = $element['#id'];
// Special treatment for link fields.
if ($element['#type'] == 'link_field') {
$field_id = $element['#id'] . '-url';
}
$field_js = array(
'data' => array(
'linkit' => array(
'fields' => array(
$field_id => array(
'profile' => $instance['settings']['linkit']['profile'],
'insert_plugin' => $profile->data['insert_plugin']['plugin'],
'url_method' => $profile->data['insert_plugin']['url_method'],
// @TODO: Add autocomplete settings.
),
),
),
),
'type' => 'setting',
);
// Link fields can have a title field.
if ($element['#type'] == 'link_field') {
if (isset($instance['settings']['title']) && in_array($instance['settings']['title'], array('optional', 'required'))) {
$field_js['data']['linkit']['fields'][$field_id]['title_field'] = $element['#id'] . '-title';
}
}
// Attach js files and settings Linkit needs.
$element['#attached']['library'][] = array('linkit', 'base');
$element['#attached']['library'][] = array('linkit', 'field');
$element['#attached']['js'][] = $insert_plugin['javascript'];
$element['#attached']['js'][] = $field_js;
$button_text = !empty($instance['settings']['linkit']['button_text']) ? $instance['settings']['linkit']['button_text'] : t('Search');
// Add Linkit dialog button to the element suffix.
$element['#field_suffix'] = '<a class="button linkit-field-button linkit-field-' . $field_id . '" href="#">' . $button_text . '</a>';
}
return $element;
}
@@ -0,0 +1,31 @@
name = Linkit
description = Provides an easy interface for inserting links.
core = 7.x
configure = admin/config/content/linkit
dependencies[] = ctools
dependencies[] = entity
; Our classes, interfaces and abstract implementations.
files[] = includes/profile.class.php
files[] = includes/search_plugin.class.php
files[] = plugins/linkit_search/entity.class.php
files[] = plugins/linkit_search/file.class.php
files[] = plugins/linkit_search/node.class.php
files[] = plugins/linkit_search/taxonomy_term.class.php
files[] = plugins/linkit_search/user.class.php
; Test files
files[] = test/linkit.test
files[] = test/linkit_profile.test
files[] = test/linkit_search_plugin.test
files[] = test/linkit_search_plugin_node.test
files[] = test/linkit_search_plugin_user.test
files[] = test/linkit_search_plugin_taxonomy_term.test
files[] = test/linkit_field.test
; Information added by Drupal.org packaging script on 2015-01-06
version = "7.x-3.3"
core = "7.x"
project = "linkit"
datestamp = "1420580858"
@@ -0,0 +1,223 @@
<?php
/**
* @file
* Install, update and uninstall functions for the Linkit module.
*/
/**
* Implements hook_schema().
*/
function linkit_schema() {
$schema = array();
$schema['linkit_profiles'] = array(
'description' => 'Base table holding Linkit profiles.',
'export' => array(
'key' => 'name',
'key name' => 'Name',
'primary key' => 'pid',
'object' => 'LinkitProfile',
'identifier' => 'linkit_profile',
'status' => 'linkit_profiles_status',
'load callback' => 'linkit_profile_load',
'load all callback' => 'linkit_profile_load_all',
'bulk export' => TRUE,
'api' => array(
'owner' => 'linkit',
'api' => 'linkit_profiles',
'minimum_version' => 1,
'current_version' => 1,
),
),
'fields' => array(
'pid' => array(
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
'no export' => TRUE,
'description' => 'Serial id for this profile.',
),
'name' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'description' => 'Machine-readable name for this profile.',
),
'admin_title' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'description' => 'Administrative title for this profile.',
),
'admin_description' => array(
'type' => 'text',
'not null' => TRUE,
'size' => 'medium',
'description' => 'Administrative description for this profile.',
),
'profile_type' => array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'description' => 'The profile type.',
),
'data' => array(
'type' => 'blob',
'size' => 'big',
'not null' => TRUE,
'serialize' => TRUE,
'description' => 'Serialized data containing the profile settings.',
),
),
'primary key' => array('pid'),
'unique keys' => array(
'name' => array('name'),
),
'indexes' => array(
'pid' => array('pid'),
'profile_type' => array('profile_type'),
),
);
return $schema;
}
/**
* Migrate settings from v2 to v3 if needed.
*/
function linkit_update_7300() {
if (!db_field_exists('linkit_profiles', 'role_rids')) {
// Already 3.x, no need for migration from 2.x.
return;
}
// Get old profiles.
$old_profiles = db_query("SELECT * FROM {linkit_profiles} ORDER BY weight DESC");
//Drop redundant fields
db_drop_field('linkit_profiles', 'role_rids');
db_drop_field('linkit_profiles', 'weight');
db_add_field('linkit_profiles', 'profile_type',
array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'description' => 'The profile type.',
'default' => ''
)
);
db_add_field('linkit_profiles', 'admin_description',
array(
'type' => 'text',
'size' => 'medium',
'description' => 'Administrative description for this profile.',
)
);
foreach ($old_profiles as $profile) {
$data = unserialize($profile->data);
// Rename the plugins
$data['search_plugins'] = $data['plugins'];
unset($data['plugins']);
$data['attribute_plugins'] = $data['attributes'];
unset($data['plugins']);
$data['attribute_plugins']['target'] = array(
'enabled' => 0,
'weight' => -10,
);
// Add new plugins
$data['insert_plugin'] = array(
'plugin' => 'raw_url',
'url_method' => 1,
);
// Remove reverse_menu_trail
foreach ($data as $key => $item) {
if(strstr($key, 'entity:')) {
unset($data[$key]['reverse_menu_trail']);
}
}
$profile->data = serialize($data);
// All old profiles are migrated as field profiles
// Do the update.
db_update('linkit_profiles')
->fields(array(
'data' => $profile->data,
'profile_type' => "2"
))
->condition('pid', $profile->pid)
->execute();
// Store the weightest profile
$weightest_profile = clone $profile;
// Insert an editor profile for every field profile
// Copy the prfoile for latter usage
$profile_editor = clone $profile;
$profile_editor->pid = NULL;
$data = unserialize($profile_editor->data);
$data['text_formats'] = array(
'full_html' => 'full_html',
'filtered_html' => 0,
'plain_text' => 0
);
$profile_editor->data = serialize($data);
$profile_editor->name = $profile_editor->name . '_editor';
$profile_editor->admin_title = $profile_editor->admin_title . ' [editor]';
$profile_editor->profile_type = 1;
$profile_editor->admin_description = '';
unset($profile_editor->role_rids, $profile_editor->weight);
db_insert('linkit_profiles')
->fields((array)$profile_editor)
->execute();
}
// Update the field instances with the weightest profile
$instances_info = field_info_instances();
foreach ($instances_info as $entity_type_name => $entity_type) {
foreach ($entity_type as $bundle_name => $bundle) {
foreach ($bundle as $field_name => $field) {
if(isset($field['settings'], $field['settings']['linkit'])) {
$settings = &$field['settings']['linkit'];
$settings['button_text'] = 'Search';
unset($settings['insert_plugin']);
if($settings['enable']) {
$settings['profile'] = $weightest_profile->name;
}
else {
$settings['profile'] = '';
}
field_update_instance($field);
}
}
}
}
// Rebuild code registry so the LinkitProfile class is found.
registry_rebuild();
}
/**
* Do nothing. Update the schema version.
*/
function linkit_update_7301() {
// Do nothing.
}
/**
* Set URL type to "Entity view page" to preserve current behavior.
*/
function linkit_update_7302() {
require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'linkit') . '/plugins/linkit_search/file.class.php';
$profiles = linkit_profile_load_all();
foreach ($profiles as $profile) {
if (isset($profile->data['entity:file'])) {
$profile->data['entity:file'] = LINKIT_FILE_URL_TYPE_ENTITY;
ctools_export_crud_save('linkit_profiles', $profile);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains the Linkit profile export UI integration code.
*/
/**
* Linktit profile export UI class handler for Views UI.
*/
class linkit_profiles extends ctools_export_ui {
/**
* @TODO: Can we call parent:list_build_row and then just add linkit specific
* columns?
*/
function list_build_row($item, &$form_state, $operations) {
// Set up sorting
$name = $item->{$this->plugin['export']['key']};
$schema = ctools_export_get_schema($this->plugin['schema']);
// Note: $item->{$schema['export']['export type string']} should have already been set up by export.inc so
// we can use it safely.
switch ($form_state['values']['order']) {
case 'disabled':
$this->sorts[$name] = empty($item->disabled) . $name;
break;
case 'title':
$this->sorts[$name] = $item->{$this->plugin['export']['admin_title']};
break;
case 'name':
$this->sorts[$name] = $name;
break;
case 'storage':
$this->sorts[$name] = $item->{$schema['export']['export type string']} . $name;
break;
}
$this->rows[$name]['data'] = array();
$this->rows[$name]['class'] = !empty($item->disabled) ? array('ctools-export-ui-disabled') : array('ctools-export-ui-enabled');
// If we have an admin title, make it the first row.
if (!empty($this->plugin['export']['admin_title'])) {
$this->rows[$name]['data'][] = array('data' => check_plain($item->{$this->plugin['export']['admin_title']}), 'class' => array('ctools-export-ui-title'));
}
// Profile type.
$this->rows[$name]['data'][] = array('data' => linkit_get_profile_type($item->profile_type), 'class' => array('linkit-export-ui-profile-type'));
// Storage.
$this->rows[$name]['data'][] = array('data' => check_plain($item->{$schema['export']['export type string']}), 'class' => array('ctools-export-ui-storage'));
// Operations.
$ops = theme('links__ctools_dropbutton', array('links' => $operations, 'attributes' => array('class' => array('links', 'inline'))));
$this->rows[$name]['data'][] = array('data' => $ops, 'class' => array('ctools-export-ui-operations'));
// Add an automatic mouseover of the description if one exists.
$this->rows[$name]['title'] = $item->{$this->plugin['export']['admin_description']};
}
function list_table_header() {
$header = array();
$header[] = array('data' => t('Title'), 'class' => array('ctools-export-ui-title'));
$header[] = array('data' => t('Profile type'), 'class' => array('ctools-export-ui-profile-type'));
$header[] = array('data' => t('Storage'), 'class' => array('ctools-export-ui-storage'));
$header[] = array('data' => t('Operations'), 'class' => array('ctools-export-ui-operations'));
return $header;
}
}
@@ -0,0 +1,501 @@
<?php
/**
* @file
* Ctools export ui plugin to handle Linkit profiles.
*/
$plugin = array(
'schema' => 'linkit_profiles',
'access' => 'administer linkit',
'menu' => array(
'menu prefix' => 'admin/config/content',
'menu item' => 'linkit',
'menu title' => 'Linkit Profiles',
'menu description' => t('Manage Linkit profiles.'),
),
'title singular' => t('Linkit profile'),
'title plural' => t('Linkit profiles'),
'title singular proper' => t('Linkit profile'),
'title plural proper' => t('Linkit profiles'),
'handler' => 'linkit_profiles',
);
/**
* Linkit profile settings form.
*
* @param $form
* A nested array of form elements that comprise the form.
* @param $form_state
* An associative array containing the current state of the form.
*/
function linkit_profiles_form(&$form, &$form_state) {
$profile = $form_state['item'];
// Set data as a tree.
$form['data'] = array(
'#tree' => TRUE,
'#type' => 'vertical_tabs',
);
// Append profile type settings to the profile form.
_linkit_build_profile_type_form_fields($form, $profile);
// Append search plugin form element to the profile form.
_linkit_build_search_plugin_form_fields($form, $profile);
// Append insert plugin form element to the profile form.
_linkit_build_insert_plugin_form_fields($form, $profile);
// Append attribute plugin form element to the profile form.
_linkit_build_attribute_plugin_form_fields($form, $profile);
// Append IMCE plugin form element to the profile form.
if (module_exists('imce')) {
_linkit_build_imce_form_fields($form, $profile);
}
// Append autocomplete (BAC) form element to the profile form.
_linkit_build_autocomplete_form_fields($form, $profile);
}
/**
* Linkit profile validate callback.
*
* @see linkit_profiles_form()
*/
function linkit_profiles_form_validate(&$form, &$form_state) {
// If the profile type is field, then insert plugin is required.
if ($form_state['values']['profile_type'] == LINKIT_PROFILE_TYPE_FIELD
&& empty($form_state['values']['data']['insert_plugin']['plugin'])) {
form_set_error('data][insert_plugin][plugin', t('You have to select a insert plugin for profiles used with fields.'));
}
}
/**
* Linkit profile submit callback.
*
* @see linkit_profiles_form()
*/
function linkit_profiles_form_submit(&$form, &$form_state) {
// Unset some variables used by "field profiles" to make it clean.
if ($form_state['values']['profile_type'] == LINKIT_PROFILE_TYPE_EDITOR) {
unset($form_state['values']['data']['insert_plugin']['plugin']);
}
// Unset some variables used by "editor profiles" to make it clean.
if ($form_state['values']['profile_type'] == LINKIT_PROFILE_TYPE_FIELD) {
unset($form_state['values']['data']['text_formats']);
}
// Unset active tab state, we dont need to save this.
unset($form_state['values']['data']['data__active_tab']);
}
/**
* Append profile type form element to the profile form.
*
* @param LinkitProfile $profile
* A profile object contains all settings for the profile.
*
* @see linkit_profiles_form()
*/
function _linkit_build_profile_type_form_fields(&$form, LinkitProfile $profile) {
$form['data']['profile_type'] = array(
'#type' => 'fieldset',
'#title' => t('Profile type'),
'#description' => t(''),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#tree' => FALSE,
);
$form['data']['profile_type']['profile_type'] = array(
'#title' => t("This profile will be used with"),
'#type' => 'radios',
'#title_display' => 'before',
'#options' => array(
LINKIT_PROFILE_TYPE_EDITOR => t('Editors'),
LINKIT_PROFILE_TYPE_FIELD => t('Fields'),
),
LINKIT_PROFILE_TYPE_EDITOR => array(
'#description' => t('This profile will be used with editors like CKeditor or TinyMCE.'),
),
LINKIT_PROFILE_TYPE_FIELD => array(
'#description' => t('This profile will be used with fields that do not have any editors.'),
),
'#default_value' => isset($profile->profile_type) ? $profile->profile_type : 'editor',
'#required' => TRUE,
'#parents' => array('profile_type'),
);
$formats = array();
foreach(filter_formats() as $format) {
$formats[$format->format] = $format->name;
}
$form['data']['profile_type']['text_formats'] = array(
'#type' => 'checkboxes',
'#title' => t('Attach this profile to one or more text formats'),
'#options' => $formats,
'#empty_option' => t('- Select one or more text format- '),
'#default_value' => isset($profile->data['text_formats'])
? $profile->data['text_formats'] : array(),
'#description' => t('.'),
'#states' => array(
'visible' => array(
// While waiting for https://drupal.org/node/879580 to be commited, we
// have to convert the constant to a string.
'input[name="profile_type"]' => array('value' => (string) LINKIT_PROFILE_TYPE_EDITOR),
),
),
'#parents' => array('data', 'text_formats'),
);
}
/**
* Append search plugin form element to the profile form.
*
* @param LinkitProfile $profile
* A profile object contains all settings for the profile.
*
* @see linkit_profiles_form()
*/
function _linkit_build_search_plugin_form_fields(&$form, LinkitProfile $profile) {
// Load all search plugins.
$search_plugins = linkit_search_plugin_load_all();
$form['data']['search_plugins_fieldset'] = array(
'#type' => 'fieldset',
'#title' => t('Search plugins'),
'#description' => t('Linkit is all about the search plugins. They define what content Linkit will present in the autocomplete search field.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#tree' => FALSE,
);
$form['data']['search_plugins_fieldset']['search_plugins'] = array(
'#markup' => '',
'#tree' => TRUE,
'#parents' => array('data', 'search_plugins'),
'#theme' => 'linkit_plugin_form_table',
);
$form['data']['search_plugins_fieldset']['search_plugins_settings'] = array(
'#markup' => '',
'#tree' => TRUE,
'#parents' => array('data'),
);
// Used to store plugin form elements temporary so we can use this to sort by
// weight later.
$tmp = array();
foreach ($search_plugins AS $name => $plugin_definition) {
// Get a plugin instance.
$plugin = LinkitSearchPlugin::factory($plugin_definition, $profile);
$tmp[$name]['name'] = array('#markup' => $plugin->ui_title());
$tmp[$name]['description'] = array('#markup' => $plugin->ui_description());
$tmp[$name]['enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Enable @title', array('@title' => $plugin->ui_title())),
'#title_display' => 'invisible',
'#default_value' => isset($profile->data['search_plugins'][$name]['enabled'])
? $profile->data['search_plugins'][$name]['enabled'] : FALSE,
);
$tmp[$name]['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight for @title', array('@title' => $plugin->ui_title())),
'#title_display' => 'invisible',
'#default_value' => isset($profile->data['search_plugins'][$name]['weight'])
? $profile->data['search_plugins'][$name]['weight'] : '',
);
// Append the search plugin specific settings.
$plugin_specific_settings = $plugin->buildSettingsForm();
if ($plugin_specific_settings) {
// Add the handler settings form to the generic.
$form['data']['search_plugins_fieldset']['search_plugins_settings'] += $plugin_specific_settings;
}
}
// Sort by weight.
uasort($tmp, '_linkit_sort_plugins_by_weight_default_value');
foreach ($tmp AS $name => $element) {
$form['data']['search_plugins_fieldset']['search_plugins'][$name] = $element;
}
}
/**
* Append insert plugin form element to the profile form.
*
* @param LinkitProfile $profile
* A profile object contains all settings for the profile.
*
* @see linkit_profiles_form()
*
* @TODO: Insert pluings should only be used when dealing with fields as it
* makes no sence to have them when using a wysiwyg editor.
*/
function _linkit_build_insert_plugin_form_fields(&$form, LinkitProfile $profile) {
// Make a list of all insert plugins.
$insert_plugins = array();
foreach (linkit_insert_plugin_load_all() as $name => $plugin) {
$insert_plugins[$name] = $plugin['name'];
}
$form['data']['insert_plugin'] = array(
'#type' => 'fieldset',
'#title' => t('Insert methods'),
'#description' => t(''),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#tree' => TRUE,
);
// Settings for insert plugins.
$form['data']['insert_plugin']['plugin'] = array(
'#type' => 'select',
'#title' => t('Insert plugin'),
'#options' => $insert_plugins,
'#empty_option' => t('- Select an insert plugin -'),
'#default_value' => isset($profile->data['insert_plugin']['plugin'])
? $profile->data['insert_plugin'] : '',
'#states' => array(
'visible' => array(
'input[name="profile_type"]' => array('value' => LINKIT_PROFILE_TYPE_FIELD),
),
'required' => array(
'input[name="profile_type"]' => array('value' => LINKIT_PROFILE_TYPE_FIELD),
),
),
);
$form['data']['insert_plugin']['url_method'] = array(
'#title' => t("Insert paths as:"),
'#type' => 'radios',
'#title_display' => 'before',
'#options' => array(
LINKIT_URL_METHOD_RAW => t('Raw paths'),
LINKIT_URL_METHOD_RAW_SLASH => t('Raw paths, with a slash (/) in the beginning.'),
),
LINKIT_URL_METHOD_RAW => array(
'#description' => t('This will insert Drupal internal paths like
%example_link which is probably what you want if you are going to use
this profile together with <strong>fields</strong>.
<br/>If used in textareas, these paths typically need to be formatted
using a input filter, but provides more flexibility in a multilingual
site or when moving a site.', array('%example_link' => 'node/123')),
),
LINKIT_URL_METHOD_RAW_SLASH => array(
'#description' => t('This is the default behavior, it will insert paths
like %example_link.<br/>These will work <strong>without the need for a input filter</strong>,
but might fail after moving a site, or on multilingual sites.',
array('%example_link' => '/node/123')),
),
'#default_value' => isset($profile->data['insert_plugin']['url_method']) ?
$profile->data['insert_plugin']['url_method'] : LINKIT_URL_METHOD_RAW_SLASH,
'#description' => t('These will have no effect on already created links.'),
);
if (module_exists('path')) {
$form['data']['insert_plugin']['url_method']['#options'][LINKIT_URL_METHOD_ALIAS] = t('Alias paths');
$form['data']['insert_plugin']['url_method'][LINKIT_URL_METHOD_ALIAS] = array(
'#description' => t('This will insert the alias for the result item.
<br/>The paths will be runned thru the <em>url()</em> function.', array('%example_link' => 'node/123')),
);
}
}
/**
* Append attribute plugin form element to the profile form.
*
* @param LinkitProfile $profile
* A profile object contains all settings for the profile.
*
* @see linkit_profiles_form()
*/
function _linkit_build_attribute_plugin_form_fields(&$form, LinkitProfile $profile) {
// Load all attribute plugins.
$attributes = linkit_attribute_plugin_load_all();
$form['data']['attribute_plugins_fieldset'] = array(
'#type' => 'fieldset',
'#title' => t('Attributes'),
'#description' => t('Attributes is HTML attributes that will be attached to
the insert plugin.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#tree' => FALSE,
);
$form['data']['attribute_plugins_fieldset']['attribute_plugins'] = array(
'#markup' => '',
'#tree' => TRUE,
'#parents' => array('data', 'attribute_plugins'),
'#theme' => 'linkit_plugin_form_table',
);
// Used to store plugin form elements temporary so we can use this to sort by
// weight later.
$tmp = array();
foreach ($attributes AS $name => $attribute) {
$tmp[$name]['name'] = array('#markup' => $attribute['name']);
$tmp[$name]['enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Enable @title', array('@title' => $attribute['name'])),
'#title_display' => 'invisible',
'#default_value' => isset($profile->data['attribute_plugins'][$name]['enabled'])
? $profile->data['attribute_plugins'][$name]['enabled'] : FALSE,
);
$tmp[$name]['weight'] = array(
'#type' => 'weight',
'#title' => t('Weight for @title', array('@title' => $attribute['name'])),
'#title_display' => 'invisible',
'#default_value' => isset($profile->data['attribute_plugins'][$name]['weight'])
? $profile->data['attribute_plugins'][$name]['weight'] : '',
);
}
// Sort by weight.
uasort($tmp, '_linkit_sort_plugins_by_weight_default_value');
foreach ($tmp AS $name => $element) {
$form['data']['attribute_plugins_fieldset']['attribute_plugins'][$name] = $element;
}
$form['data']['attribute_plugins_fieldset']['attribute_plugins_info'] = array(
'#markup' => 'Keep in mind that not all attributes can be used by all insert plugins.',
);
}
/**
* Append IMCE plugin form element to the profile form.
*
* @param LinkitProfile $profile
* A profile object contains all settings for the profile.
*
* @see linkit_profiles_form()
*/
function _linkit_build_imce_form_fields(&$form, LinkitProfile $profile) {
$form['data']['imce'] = array(
'#type' => 'fieldset',
'#title' => t('IMCE File Picker'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#tree' => FALSE,
'#weight' => 80,
);
$form['data']['imce']['use_imce'] = array(
'#title' => t('Enable !imce File Picker', array('!imce' => l(t('IMCE'),
'admin/config/media/imce'))),
'#type' => 'checkbox',
'#description' => t('Note that only public files are supported.'),
'#default_value' => isset($profile->data['imce'])
? $profile->data['imce'] : FALSE,
'#parents' => array('data', 'imce'),
);
}
/**
* Append autocomplete (BAC) form element to the profile form.
*
* @param LinkitProfile $profile
* A profile object contains all settings for the profile.
*
* @see linkit_profiles_form()
*/
function _linkit_build_autocomplete_form_fields(&$form, LinkitProfile $profile) {
$form['data']['autocomplete'] = array(
'#type' => 'fieldset',
'#title' => t('Autocomplete options'),
'#description' => t('Linkit uses !bac which may be configured with focus on
performance.', array('!bac' => l(t('Better Autocomplete'),
'https://github.com/betamos/Better-Autocomplete'))),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#tree' => TRUE,
'#weight' => 100,
);
$form['data']['autocomplete']['charLimit'] = array(
'#title' => t('Character limit'),
'#type' => 'textfield',
'#default_value' => isset($profile->data['autocomplete']['charLimit']) ? $profile->data['autocomplete']['charLimit'] : LINKIT_CHAR_LIMIT,
'#size' => 5,
'#description' => t('Minimum number of characters to perform server polling.
A higher value reduces server load. <em>Default: @default</em>', array('@default' => LINKIT_CHAR_LIMIT)),
'#element_validate' => array('linkit_isnumeric_validate'),
);
$form['data']['autocomplete']['wait'] = array(
'#title' => t('Key press delay'),
'#type' => 'textfield',
'#default_value' => isset($profile->data['autocomplete']['wait']) ? $profile->data['autocomplete']['wait'] : LINKIT_WAIT,
'#size' => 5,
'#description' => t('Time from last key press to poll the server. A higher
value reduces server load. <em>Default: @default</em>', array('@default' => LINKIT_WAIT)),
'#field_suffix' => t('ms'),
'#element_validate' => array('linkit_isnumeric_validate'),
);
$form['data']['autocomplete']['remoteTimeout'] = array(
'#title' => t('Remote timeout'),
'#type' => 'textfield',
'#default_value' => isset($profile->data['autocomplete']['remoteTimeout']) ? $profile->data['autocomplete']['remoteTimeout'] : LINKIT_REMOTE_TIMEOUT,
'#size' => 5,
'#description' => t('Client side timeout for a request to the server.
<em>Default: @default</em>', array('@default' => LINKIT_REMOTE_TIMEOUT)),
'#field_suffix' => t('ms'),
'#element_validate' => array('linkit_isnumeric_validate'),
);
}
/**
* Function used by uasort to sort plugin arrays by the weight default value.
*/
function _linkit_sort_plugins_by_weight_default_value($a, $b) {
return $a["weight"]['#default_value'] >= $b["weight"]['#default_value'];
}
/**
* Extract tokens that can be used by the $type.
*
* @param $type
* A string with the entity type.
*/
function linkit_extract_tokens($type) {
// token_info() has it own static cache, so we can call it as we like.
$tokens = token_info();
// If no tokens for the type exists, return an empty array.
if (!isset($tokens['tokens'][$type])) {
return array();
}
$available_tokens = array();
foreach ($tokens['tokens'][$type] as $token_key => $token) {
$available_tokens[] = '[' . $type . ':' . $token_key . ']';
}
return $available_tokens;
}
/**
* Element validate callback for fields that should be numeric.
*
* This function is assigned as an #element_validate callback in
* linkit_profiles_form().
*/
function linkit_isnumeric_validate($element, &$form_state, $form) {
if (!empty($element['#value']) && !is_numeric($element['#value'])) {
form_error($element, $element['#title'] . ' should only contains numbers.');
}
}
@@ -0,0 +1,35 @@
<?php
/**
* @file
* Define Linkit accesskey attribute plugin.
*/
$plugin = array(
'name' => t('Access key'),
'callback' => 'linkit_attribute_plugin_accesskey',
);
/**
* Create a FAPI element.
*
* @param array $plugin
* This is the plugin definition.
*
* @param array $settings
* An array of settings for this attribute. This is from the profile settings.
*
* @return
* An FAPI element.
*
* @see LinkitProfile::setAttributes()
*/
function linkit_attribute_plugin_accesskey($plugin, $settings = array()) {
return array(
'#type' => 'textfield',
'#title' => $plugin['name'],
'#maxlength' => 255,
'#size' => 40,
'#default_value' => '',
'#weight' => isset($settings['weight']) ? $settings['weight'] : 0,
);
}
@@ -0,0 +1,35 @@
<?php
/**
* @file
* Define Linkit class attribute plugin.
*/
$plugin = array(
'name' => t('Classes'),
'callback' => 'linkit_attribute_plugin_class',
);
/**
* Create a FAPI element.
*
* @param array $plugin
* This is the plugin definition.
*
* @param array $settings
* An array of settings for this attribute. This is from the profile settings.
*
* @return
* An FAPI element.
*
* @see LinkitProfile::setAttributes()
*/
function linkit_attribute_plugin_class($plugin, $settings = array()) {
return array(
'#type' => 'textfield',
'#title' => $plugin['name'],
'#maxlength' => 255,
'#size' => 40,
'#default_value' => '',
'#weight' => isset($settings['weight']) ? $settings['weight'] : 0,
);
}
@@ -0,0 +1,35 @@
<?php
/**
* @file
* Define Linkit id attribute plugin.
*/
$plugin = array(
'name' => t('Id'),
'callback' => 'linkit_attribute_plugin_id',
);
/**
* Create a FAPI element.
*
* @param array $plugin
* This is the plugin definition.
*
* @param array $settings
* An array of settings for this attribute. This is from the profile settings.
*
* @return
* An FAPI element.
*
* @see LinkitProfile::setAttributes()
*/
function linkit_attribute_plugin_id($plugin, $settings = array()) {
return array(
'#type' => 'textfield',
'#title' => $plugin['name'],
'#maxlength' => 255,
'#size' => 40,
'#default_value' => '',
'#weight' => isset($settings['weight']) ? $settings['weight'] : 0,
);
}
@@ -0,0 +1,35 @@
<?php
/**
* @file
* Define Linkit rel attribute plugin.
*/
$plugin = array(
'name' => t('Relationship'),
'callback' => 'linkit_attribute_plugin_rel',
);
/**
* Create a FAPI element.
*
* @param array $plugin
* This is the plugin definition.
*
* @param array $settings
* An array of settings for this attribute. This is from the profile settings.
*
* @return
* An FAPI element.
*
* @see LinkitProfile::setAttributes()
*/
function linkit_attribute_plugin_rel($plugin, $settings = array()) {
return array(
'#type' => 'textfield',
'#title' => $plugin['name'],
'#maxlength' => 255,
'#size' => 40,
'#default_value' => '',
'#weight' => isset($settings['weight']) ? $settings['weight'] : 0,
);
}
@@ -0,0 +1,39 @@
<?php
/**
* @file
* Define Linkit target attribute plugin.
*/
$plugin = array(
'name' => t('Target'),
'callback' => 'linkit_attribute_plugin_target',
);
/**
* Create a FAPI element.
*
* @param array $plugin
* This is the plugin definition.
*
* @param array $settings
* An array of settings for this attribute. This is from the profile settings.
*
* @return
* An FAPI element.
*
* @see LinkitProfile::setAttributes()
*/
function linkit_attribute_plugin_target($plugin, $settings = array()) {
return array(
'#type' => 'select',
'#title' => $plugin['name'],
'#options' => array(
'' => '',
'_blank' => t('New window (_blank)'),
'_top' => t('Top window (_top)'),
'_self' => t('Same window (_self)'),
'_parent' => t('Parent window (_parent)')
),
'#weight' => isset($settings['weight']) ? $settings['weight'] : 0,
);
}
@@ -0,0 +1,35 @@
<?php
/**
* @file
* Define Linkit title attribute plugin.
*/
$plugin = array(
'name' => t('Title'),
'callback' => 'linkit_attribute_plugin_title',
);
/**
* Create a FAPI element.
*
* @param array $plugin
* This is the plugin definition.
*
* @param array $settings
* An array of settings for this attribute. This is from the profile settings.
*
* @return
* An FAPI element.
*
* @see LinkitProfile::setAttributes()
*/
function linkit_attribute_plugin_title($plugin, $settings = array()) {
return array(
'#type' => 'textfield',
'#title' => $plugin['name'],
'#maxlength' => 255,
'#size' => 40,
'#default_value' => '',
'#weight' => isset($settings['weight']) ? $settings['weight'] : 0,
);
}
@@ -0,0 +1,10 @@
<?php
/**
* @file
* Defines the HTML link insert plugin.
*/
$plugin = array(
'name' => t('HTML Link'),
'javascript' => 'html_link.js',
);
@@ -0,0 +1,35 @@
/**
* @file
* HTML Link insert plugin for Linkit.
*/
(function ($) {
Drupal.linkit.registerInsertPlugin('html_link', {
insert : function(data) {
var text,
selection = Drupal.settings.linkit.currentInstance.selection;
// Delete all attributes that are empty.
for (name in data.attributes) {
(data.attributes[name]) ? null : delete data.attributes[name];
}
if (typeof selection != 'undefined' &&
selection.text.length >= 1) {
text = selection.text;
}
else {
text = Drupal.settings.linkit.currentInstance.linkContent;
}
// Use document.createElement as it is mush fasten then $('<a/>).
return $(document.createElement('a'))
.attr(data.attributes)
.attr('href', data.path)
.html(text)
// Convert the element to a string.
.get(0).outerHTML;
}
});
})(jQuery);
@@ -0,0 +1,9 @@
<?php
/**
* @file
* Defines the markdown insert plugin.
*/
$plugin = array(
'name' => t('Markdown'),
'javascript' => drupal_get_path('module', 'linkit') . '/plugins/linkit_insert/markdown/markdown.js',
);
@@ -0,0 +1,31 @@
/**
* @file
* Markdown insert plugin for Linkit.
*
* Notes: Markdown dont support any attributes exept "title".
* [An example](http://example.com/ "Title")
*/
(function ($) {
Drupal.linkit.registerInsertPlugin('markdown', {
insert : function(data) {
var pattern = '[!text](!url!title)',
args = {
'!url' : data.path,
'!title' : data.attributes.title ? ' "' + data.attributes.title + '"' : ''
},
selection = Drupal.settings.linkit.currentInstance.selection;
if (typeof selection != 'undefined' &&
selection.text.length >= 1) {
args['!text'] = selection.text;
}
else {
args['!text'] = Drupal.settings.linkit.currentInstance.linkContent;
}
return Drupal.formatString(pattern, args);
}
});
})(jQuery);
@@ -0,0 +1,10 @@
<?php
/**
* @file
* Defines the raw url insert plugin.
*/
$plugin = array(
'name' => t('Raw URL'),
'javascript' => 'raw_url.js',
);
@@ -0,0 +1,11 @@
/**
* @file
* Raw HTML insert plugin for Linkit.
*/
(function ($) {
Drupal.linkit.registerInsertPlugin('raw_url', {
insert : function(data, settings) {
return data.path;
}
});
})(jQuery);
@@ -0,0 +1,334 @@
<?php
/**
* @file
* Define Linkit entity search plugin class.
*/
/**
* Reprecents a Linkit entity search plugin.
*/
class LinkitSearchPluginEntity extends LinkitSearchPlugin {
/**
* Entity field query instance.
*
* @var EntityFieldQuery Resource
*/
var $query;
/**
* The entity info array of an entity type.
*
* @var array
*/
var $entity_info = array();
/**
* The name of the property that contains the entity label.
*
* @var string
*/
var $entity_field_label;
/**
* The name of the property of the bundle object that contains the name of
* the bundle object.
*
* @var string
*/
var $entity_key_bundle;
/**
* Plugin specific settings.
*
* @var array
*/
var $conf = array();
/**
* Overrides LinkitSearchPlugin::__construct().
*
* Initialize this plugin with the plugin, profile, and entity specific
* variables.
*
* @param array $plugin
* The plugin array.
*
* @param LinkitProfile object $profile
* The Linkit profile to use.
*/
function __construct($plugin, LinkitProfile $profile) {
parent::__construct($plugin, $profile);
// Load the corresponding entity info.
$this->entity_info = entity_get_info($this->plugin['entity_type']);
// Set bundle key name.
if (isset($this->entity_info['entity keys']['bundle']) &&
!isset($this->entity_key_bundle)) {
$this->entity_key_bundle = $this->entity_info['entity keys']['bundle'];
}
// Set the label field name.
if (!isset($this->entity_field_label)) {
// Check that the entity has a label in entity keys.
// If not, Linkit don't know what to search for.
if (!isset($this->entity_info['entity keys']['label'])) {
// This is only used when building the plugin list.
$this->unusable = TRUE;
}
else {
$this->entity_field_label = $this->entity_info['entity keys']['label'];
}
}
// Make a shortcut for the profile data settings for this plugin.
$this->conf = isset($this->profile->data[$this->plugin['name']]) ?
$this->profile->data[$this->plugin['name']] : array();
}
/**
* Create a label of an entity.
*
* @param $entity
* The entity to get the label from.
*
* @return
* The entity label, or FALSE if not found.
*/
function createLabel($entity) {
return check_plain(entity_label($this->plugin['entity_type'], $entity));
}
/**
* Create a search row description.
*
* If there is a "result_description", run it thro token_replace.
*
* @param object $data
* An entity object that will be used in the token_place function.
*
* @return
* A string containing the row description.
*
* @see token_replace()
*/
function createDescription($data) {
$description = token_replace(check_plain($this->conf['result_description']), array(
$this->plugin['entity_type'] => $data,
), array('clear' => TRUE));
return $description;
}
/**
* Create an uri for an entity.
*
* @param $entity
* The entity to get the path from.
*
* @return
* A string containing the path of the entity, NULL if the entity has no
* uri of its own.
*/
function createPath($entity) {
// Create the URI for the entity.
$uri = entity_uri($this->plugin['entity_type'], $entity);
$options = array();
// Handle multilingual sites.
if (isset($entity->language) && $entity->language != LANGUAGE_NONE && drupal_multilingual() && language_negotiation_get_any(LOCALE_LANGUAGE_NEGOTIATION_URL)) {
$languages = language_list('enabled');
// Only use enabled languages.
$languages = $languages[1];
if ($languages && isset($languages[$entity->language])) {
$options['language'] = $languages[$entity->language];
}
}
// Process the uri with the insert pluing.
$path = linkit_get_insert_plugin_processed_path($this->profile, $uri['path'], $options);
return $path;
}
/**
* Create a group text.
*
* @param $entity
* The entity object.
*
* @return
* When "group_by_bundle" is active, we need to add the bundle name to the
* group, else just return the entity label.
*/
function createGroup($entity) {
// Get the entity label.
$group = $this->entity_info['label'];
// If the entities by this entity should be grouped by bundle, get the
// name and append it to the group.
if (isset($this->conf['group_by_bundle']) && $this->conf['group_by_bundle']) {
$bundles = $this->entity_info['bundles'];
$bundle_name = $bundles[$entity->{$this->entity_key_bundle}]['label'];
$group .= ' - ' . check_plain($bundle_name);
}
return $group;
}
/**
* Create a row class to appaned to the search result row.
*
* @param $entity
* The entity object.
*
* @return
* A string to with classes.
*/
function createRowClass($entity) {
return '';
}
/**
* Start a new EntityFieldQuery instance.
*/
function getQueryInstance() {
$this->query = new EntityFieldQuery();
$this->query->entityCondition('entity_type', $this->plugin['entity_type']);
// Add the default sort on the enity label.
$this->query->propertyOrderBy($this->entity_field_label, 'ASC');
}
/**
* Implements LinkitSearchPluginInterface::fetchResults().
*/
public function fetchResults($search_string) {
// If the $search_string is not a string, something is wrong and an empty
// array is returned.
$matches = array();
// Get the EntityFieldQuery instance.
$this->getQueryInstance();
// Add the search condition to the query object.
$this->query->propertyCondition($this->entity_field_label,
'%' . db_like($search_string) . '%', 'LIKE')
->addTag('linkit_entity_autocomplete')
->addTag('linkit_' . $this->plugin['entity_type'] . '_autocomplete');
// Add access tag for the query.
// There is also a runtime access check that uses entity_access().
$this->query->addTag($this->plugin['entity_type'] . '_access');
// Bundle check.
if (isset($this->entity_key_bundle) && isset($this->conf['bundles']) ) {
$bundles = array_filter($this->conf['bundles']);
if ($bundles) {
$this->query->propertyCondition($this->entity_key_bundle, $bundles, 'IN');
}
}
// Execute the query.
$result = $this->query->execute();
if (!isset($result[$this->plugin['entity_type']])) {
return array();
}
$ids = array_keys($result[$this->plugin['entity_type']]);
// Load all the entities with all the ids we got.
$entities = entity_load($this->plugin['entity_type'], $ids);
foreach ($entities AS $entity) {
// Check the access againt the definded entity access callback.
if (entity_access('view', $this->plugin['entity_type'], $entity) === FALSE) {
continue;
}
$matches[] = array(
'title' => $this->createLabel($entity),
'description' => $this->createDescription($entity),
'path' => $this->createPath($entity),
'group' => $this->createGroup($entity),
'addClass' => $this->createRowClass($entity),
);
}
return $matches;
}
/**
* Overrides LinkitSearchPlugin::buildSettingsForm().
*/
function buildSettingsForm() {
$form[$this->plugin['name']] = array(
'#type' => 'fieldset',
'#title' => t('!type plugin settings', array('!type' => $this->ui_title())),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#tree' => TRUE,
'#states' => array(
'invisible' => array(
'input[name="data[search_plugins][' . $this->plugin['name'] . '][enabled]"]' => array('checked' => FALSE),
),
),
);
// Get supported tokens for the entity type.
$tokens = linkit_extract_tokens($this->plugin['entity_type']);
// A short description in within the search result for each row.
$form[$this->plugin['name']]['result_description'] = array(
'#title' => t('Result description'),
'#type' => 'textfield',
'#default_value' => isset($this->conf['result_description']) ? $this->conf['result_description'] : '',
'#size' => 120,
'#maxlength' => 255,
'#description' => t('Available tokens: %tokens.', array('%tokens' => implode(', ', $tokens))),
);
// If the token module is installed, lets make some fancy stuff with the
// token chooser.
if (module_exists('token')) {
// Unset the regular description if token module is enabled.
unset($form[$this->plugin['name']]['result_description']['#description']);
// Display the user documentation of placeholders.
$form[$this->plugin['name']]['token_help'] = array(
'#title' => t('Replacement patterns'),
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form[$this->plugin['name']]['token_help']['help'] = array(
'#theme' => 'token_tree',
'#token_types' => array($this->plugin['entity_type']),
);
}
// If there is bundles, add some default settings features.
if (count($this->entity_info['bundles']) > 1) {
$bundles = array();
// Extract the bundle data.
foreach ($this->entity_info['bundles'] as $bundle_name => $bundle) {
$bundles[$bundle_name] = $bundle['label'];
}
// Filter the possible bundles to use if the entity has bundles.
$form[$this->plugin['name']]['bundles'] = array(
'#title' => t('Type filter'),
'#type' => 'checkboxes',
'#options' => $bundles,
'#default_value' => isset($this->conf['bundles']) ? $this->conf['bundles'] : array(),
'#description' => t('If left blank, all types will appear in autocomplete results.'),
);
// Group the results with this bundle.
$form[$this->plugin['name']]['group_by_bundle'] = array(
'#title' => t('Group by bundle'),
'#type' => 'checkbox',
'#default_value' => isset($this->conf['group_by_bundle']) ? $this->conf['group_by_bundle'] : 0,
);
}
return $form;
}
}
@@ -0,0 +1,89 @@
<?php
/**
* @file
* Linkit Entity Search Plugin.
*/
/**
* Plugin structure is an associative array containing:
* - ui_title: The title of the plugin used in the admin UI.
* - ui_description: A description used in the admin UI.
* - entity_type: (Only required if the plugin extends LinkitPluginEntity) The
* entity type this plugin will be used for.
* - handler: Describes the class to be used for this plugin.
*/
$plugin = array(
'get child' => 'linkit_entity_ctools_linkit_get_child',
'get children' => 'linkit_entity_ctools_linkit_get_children',
'handler' => array(
'class' => 'LinkitSearchPluginEntity',
'file' => 'entity.class.php',
),
);
/**
* Get a single Linkit search plugin.
*/
function linkit_entity_ctools_linkit_get_child($plugin, $parent, $child) {
$plugins = linkit_entity_ctools_linkit_get_children($plugin, $parent);
return $plugins[$parent . ':' . $child];
}
/**
* Get all child Linkit search plugins.
*/
function linkit_entity_ctools_linkit_get_children($plugin, $parent) {
$entities = entity_get_info();
$plugins = array();
// The alternative plugins is extensions of the provided
// LinkitSearchPluginEntity class provided by Linkit.
$alternative_plugins = array(
'entity:node',
'entity:user',
'entity:taxonomy_term',
'entity:file',
);
foreach ($entities as $entity_type => $entity) {
// The entity must have an URI CALLBACK defined.
if (!isset($entity['uri callback'])) {
continue;
}
$plugin_child = $plugin;
$plugin_child['ui_title'] = $entity['label'];
$plugin_child['ui_description'] = t('Extend Linkit with @entity support.', array('@entity' => $entity_type));
$plugin_child['name'] = $parent . ':' . $entity_type;
$plugin_child['entity_type'] = $entity_type;
// If there is an alternative search class for this entity, change the handler.
if (in_array('entity:' . $entity_type, $alternative_plugins)) {
$handler = array(
'class' => 'LinkitSearchPlugin' . drupal_ucfirst($entity_type),
'file' => $entity_type . '.class.php',
);
$plugin_child['handler'] = $handler;
}
drupal_alter('linkit_search_plugin_entity', $plugin_child, $entity);
$plugins[$parent . ':' . $entity_type] = $plugin_child;
}
drupal_alter('linkit_search_plugin_entities', $plugins);
// Test the plugins.
foreach ($plugins as $plugin_name => $plugin) {
// In some cases the plugins isn't auto loaded. Ctools handle this for us.
// This is typical when installing Linkit.
if (!class_exists($plugin['handler']['class'])) {
continue;
}
$tested_plugin = new $plugin['handler']['class']($plugin, new LinkitProfile());
if (isset($tested_plugin->unusable) && $tested_plugin->unusable) {
unset($plugins[$plugin_name]);
}
}
return $plugins;
}
@@ -0,0 +1,219 @@
<?php
/**
* @file
* Define Linkit file search plugin class.
*/
/**
* Define that file urls should link directly to the file itself.
*/
define('LINKIT_FILE_URL_TYPE_DIRECT', 'direct');
/**
* Define that file urls should force download of the file.
*/
define('LINKIT_FILE_URL_TYPE_DOWNLOAD', 'download');
/**
* Define that file urls should link the file entity view page.
*/
define('LINKIT_FILE_URL_TYPE_ENTITY', 'entity');
/**
* Reprecents a Linkit file search plugin.
*/
class LinkitSearchPluginFile extends LinkitSearchPluginEntity {
/**
* Overrides LinkitSearchPlugin::ui_title().
*/
function ui_title() {
return t('Managed files');
}
/**
* Overrides LinkitSearchPlugin::ui_description().
*/
function ui_description() {
return t('Extend Linkit with file support (Managed files).');
}
/**
* Overrides LinkitSearchPluginEntity::createDescription().
*
* If the file is an image, a small thumbnail can be added to the description.
* Also, image dimensions can be shown.
*/
function createDescription($data) {
$description_array = array();
// Get image info.
$imageinfo = image_get_info($data->uri);
// Add small thumbnail to the description.
if ($this->conf['image_extra_info']['thumbnail']) {
$image = $imageinfo ? theme_image_style(array(
'width' => $imageinfo['width'],
'height' => $imageinfo['height'],
'style_name' => 'linkit_thumb',
'path' => $data->uri,
)) : '';
}
// Add image dimensions to the description.
if ($this->conf['image_extra_info']['dimensions'] && !empty($imageinfo)) {
$description_array[] = $imageinfo['width'] . 'x' . $imageinfo['height'] . 'px';
}
$description_array[] = parent::createDescription($data);
// Add tiel files scheme to the description.
if ($this->conf['show_scheme']) {
$description_array[] = file_uri_scheme($data->uri) . '://';
}
$description = (isset($image) ? $image : '') . implode('<br />' , $description_array);
return $description;
}
/**
* Overrides LinkitSearchPluginEntity::createGroup().
*/
function createGroup($entity) {
// The the standard group name.
$group = parent::createGroup($entity);
// Add the scheme.
if ($this->conf['group_by_scheme']) {
// Get all stream wrappers.
$stream_wrapper = file_get_stream_wrappers();
$group .= ' - ' . $stream_wrapper[file_uri_scheme($entity->uri)]['name'];
}
return $group;
}
/**
* Overrides LinkitSearchPluginEntity::createPath().
*
* If 'Direct download' is enabled, make the link point to the file entity
* download endpoint.
*/
function createPath($entity) {
$url_type = isset($this->conf['url_type']) ? $this->conf['url_type'] : $this->getDefaultUrlType();
// We can only support the download type if we have version 2.x of the file_entity module.
if ($url_type == LINKIT_FILE_URL_TYPE_DOWNLOAD && !(module_exists('file_entity') && function_exists('file_entity_download_uri'))) {
$url_type = $this->getDefaultUrlType();
}
switch ($url_type) {
case LINKIT_FILE_URL_TYPE_DIRECT:
// Check if this is a local file.
$wrapper = file_stream_wrapper_get_instance_by_uri($entity->uri);
if ($wrapper instanceof DrupalLocalStreamWrapper) {
// Create a relative URL to the local file.
// See https://www.drupal.org/node/837794.
$path = $wrapper->getDirectoryPath() . '/' . file_uri_target($entity->uri);
}
else {
$path = file_create_url($entity->uri);
}
// Process the uri with the insert plugin.
return linkit_get_insert_plugin_processed_path($this->profile, $path, array('language' => (object) array('language' => FALSE)));
case LINKIT_FILE_URL_TYPE_DOWNLOAD:
$uri = file_entity_download_uri($entity);
// Hack for LINKIT_URL_METHOD_RAW, which won't include the options that
// we pass to linkit_get_insert_plugin_processed_path().
if (isset($uri['options']['query']['token']) && $this->profile->data['insert_plugin']['url_method'] == LINKIT_URL_METHOD_RAW) {
return $uri['path'] . '?token=' . rawurlencode($uri['options']['query']['token']);
}
// Process the uri with the insert plugin.
return linkit_get_insert_plugin_processed_path($this->profile, $uri['path'], $uri['options']);
case LINKIT_FILE_URL_TYPE_ENTITY:
// Pass back to the parent if the user wants entity urls.
return parent::createPath($entity);
}
}
/**
* Overrides LinkitSearchPluginEntity::getQueryInstance().
*/
function getQueryInstance() {
// Call the parent getQueryInstance method.
parent::getQueryInstance();
// Only search for permanent files.
$this->query->propertyCondition('status', FILE_STATUS_PERMANENT);
}
/**
* Overrides LinkitSearchPluginEntity::buildSettingsForm().
*/
function buildSettingsForm() {
$form = parent::buildSettingsForm();
$form['entity:file']['show_scheme'] = array(
'#title' => t('Show file scheme'),
'#type' => 'checkbox',
'#default_value' => isset($this->conf['show_scheme']) ? $this->conf['show_scheme'] : '',
);
$form['entity:file']['group_by_scheme'] = array(
'#title' => t('Group files by scheme'),
'#type' => 'checkbox',
'#default_value' => isset($this->conf['group_by_scheme']) ? $this->conf['group_by_scheme'] : '',
);
$form['entity:file']['url_type'] = array(
'#title' => t('URL type'),
'#type' => 'radios',
'#options' => array(
LINKIT_FILE_URL_TYPE_DIRECT => t('Direct file link'),
LINKIT_FILE_URL_TYPE_DOWNLOAD => t('Download file link'),
LINKIT_FILE_URL_TYPE_ENTITY => t('Entity view page'),
),
'#default_value' => isset($this->conf['url_type']) ? $this->conf['url_type'] : $this->getDefaultUrlType(),
);
// We can only support the download type if we have version 2.x of the file_entity module.
if (!(module_exists('file_entity') && function_exists('file_entity_download_uri'))) {
unset($form['entity:file']['url_type']['#options'][LINKIT_FILE_URL_TYPE_DOWNLOAD]);
}
$image_extra_info_options = array(
'thumbnail' => t('Show thumbnails <em>(using the image style !linkit_thumb_link)</em>', array('!linkit_thumb_link' => l(t('linkit_thumb'), 'admin/config/media/image-styles/edit/linkit_thumb'))),
'dimensions' => t('Show pixel dimensions'),
);
$form['entity:file']['image_extra_info'] = array(
'#title' => t('Images'),
'#type' => 'checkboxes',
'#options' => $image_extra_info_options,
'#default_value' => isset($this->conf['image_extra_info']) ? $this->conf['image_extra_info'] : array('thumbnail', 'dimensions'),
);
return $form;
}
/**
* Gets the default URL type to use if no URL type has been explicitly set.
*
* @return string The URL type
*/
protected function getDefaultUrlType() {
$info = entity_get_info('file');
$callback = $info['uri callback'];
if ($callback == 'entity_metadata_uri_file') {
// The Drupal core file URI callback would be used if we were to use the
// "Entity view page" URL mode, which generates absolute URLs to files.
// The "Direct link" URL mode is preferable in this case as it generates
// relative links.
return LINKIT_FILE_URL_TYPE_DIRECT;
}
else {
// We use a custom URI callback such as in Media 1.x or File Entity 2.x.
// The "Entity view page" URL mode is preferable in this case.
return LINKIT_FILE_URL_TYPE_ENTITY;
}
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @file
* Define Linkit node search plugin class.
*/
/**
* Reprecents a Linkit node search plugin.
*/
class LinkitSearchPluginNode extends LinkitSearchPluginEntity {
/**
* Overrides LinkitSearchPluginEntity::createRowClass().
*
* Adds an extra class if the node is unpublished.
*/
function createRowClass($entity) {
if ($this->conf['include_unpublished'] && $entity->status == NODE_NOT_PUBLISHED) {
return 'unpublished-node';
}
}
/**
* Overrides LinkitSearchPluginEntity::getQueryInstance().
*/
function getQueryInstance() {
// Call the parent getQueryInstance method.
parent::getQueryInstance();
// If we don't want to include unpublished nodes, add a condition on status.
if ($this->conf['include_unpublished'] == 0) {
$this->query->propertyCondition('status', NODE_PUBLISHED);
}
}
/**
* Overrides LinkitSearchPlugin::buildSettingsForm().
*/
function buildSettingsForm() {
// Get the parent settings form.
$form = parent::buildSettingsForm();
$form[$this->plugin['name']]['include_unpublished'] = array(
'#title' => t('Include unpublished nodes'),
'#type' => 'checkbox',
'#default_value' => isset($this->conf['include_unpublished']) ? $this->conf['include_unpublished'] : 0,
'#description' => t('In order to see unpublished nodes, the user most also have permissions to do so. '),
);
return $form;
}
}
@@ -0,0 +1,96 @@
<?php
/**
* @file
* Define Linkit term search plugin class.
*/
/**
* Reprecents a Linkit term search plugin.
*/
class LinkitSearchPluginTaxonomy_term extends LinkitSearchPluginEntity {
/**
* Overrides LinkitSearchPluginEntity::__construct().
*/
function __construct($plugin, $profile) {
/**
* The term entity doesn't use the same column name as in the entity keys
* bundle definition, so lets add it our self.
*/
$this->entity_key_bundle = 'vid';
parent::__construct($plugin, $profile);
}
/**
* Overrides LinkitSearchPluginEntity::createDescription().
*/
function createDescription($data) {
$description = token_replace(check_plain($this->conf['result_description']), array(
'term' => $data,
), array('clear' => TRUE));
return $description;
}
/**
* Overrides LinkitSearchPluginEntity::createGroup().
*/
function createGroup($entity) {
// Get the entity label.
$group = $this->entity_info['label'];
if (isset($this->conf['group_by_bundle']) && $this->conf['group_by_bundle']) {
$bundles = $this->entity_info['bundles'];
$bundle_name = $bundles[$entity->vocabulary_machine_name]['label'];
$group .= ' - ' . check_plain($bundle_name);
}
return $group;
}
/**
* Overrides LinkitSearchPluginEntity::fetchResults().
*/
function fetchResults($search_string) {
// The term entity doesn't use the entity keys bundle definition, its using
// the vid instead, so lets 'translate' the bundle names to vids.
if (isset($this->entity_key_bundle) && isset($this->conf['bundles']) ) {
$bundles = array_filter($this->conf['bundles']);
// Get all vocabularies.
$vocabularies = taxonomy_vocabulary_get_names();
// Temp storage for values.
$tmp_bundles = array();
foreach ($bundles as $bundle) {
$tmp_bundles[] = $vocabularies[$bundle]->{$this->entity_key_bundle};
}
// Assign the new values as the bundles.
$this->conf['bundles'] = $tmp_bundles;
}
// Call the parent.
return parent::fetchResults($search_string);
}
/**
* Overrides LinkitSearchPlugin::buildSettingsForm().
*/
function buildSettingsForm() {
$form = parent::buildSettingsForm();
// The entity plugin uses the entity name for the #token_types, but terms
// is a special case, its name is "Taxonomy_term" and the tokens are defined
// (in the taxonomy module) with just "term".
// If the token modules is installed.
if (isset($form[$this->plugin['name']]['token_help']['help']['#token_types'])) {
$form[$this->plugin['name']]['token_help']['help']['#token_types'] = array('term');
}
// If the token module is not installed, use the orignal tokens provided by
// Drupal core.
else {
// Get supported tokens for the term entity type.
$tokens = linkit_extract_tokens('term');
$form[$this->plugin['name']]['result_description']['#description'] = t('Available tokens: %tokens.', array('%tokens' => implode(', ', $tokens)));
}
return $form;
}
}
@@ -0,0 +1,24 @@
<?php
/**
* @file
* Define Linkit user search plugin class.
*/
/**
* Reprecents a Linkit user search plugin.
*/
class LinkitSearchPluginUser extends LinkitSearchPluginEntity {
/**
* Overrides LinkitSearchPluginEntity::__construct().
*/
function __construct($plugin, $profile) {
/**
* The user entity doesn't add any label in their entity keys as they define a
* "label callback" instead. Therefore we have to tell which field the user
* entity have as label.
*/
$this->entity_field_label = 'name';
parent::__construct($plugin, $profile);
}
}
@@ -0,0 +1,101 @@
<?php
/**
* @file
* Tests for Linkit module.
*/
/**
* Abstract class for Linkit testing.
*/
//abstract class LinkitTestCase extends SimpleTestCloneTestCase {
abstract class LinkitTestCase extends DrupalWebTestCase {
/**
* Permissions to apply to administers.
*
* @var array
*/
public $admin_permissions = array(
'administer linkit',
);
/**
* A user object.
*
* @var object
*/
public $account;
/**
* A Linkit profile object.
*
* @var LinkitProfile object
*/
public $_profile;
function setUp($extra_modules = array()) {
$modules = array('ctools', 'entity', 'linkit');
$modules += $extra_modules;
parent::setUp($modules);
}
/**
* Helper function: Create a Linkit profile.
*/
protected function createProfile($extra = array()) {
ctools_include('export');
$profile = ctools_export_crud_new('linkit_profiles');
$profile->name = isset($extra['name']) ? $extra['name'] : 'test_profile';
$profile->admin_title = isset($extra['admin_title']) ? $extra['admin_title'] : 'Test Profile';
$profile->admin_description = isset($extra['admin_description']) ? $extra['admin_description'] : 'This is a description for the Test Profile.';
$profile->data = array();
if (isset($extra['data'])) {
$profile->data += $extra['data'];
}
if (empty($profile->data['profile_type'])) {
$profile->profile_type = LINKIT_PROFILE_TYPE_EDITOR;
}
if (!isset($profile->data['insert_plugin']) && $profile->profile_type == LINKIT_PROFILE_TYPE_FIELD) {
$profile->data['insert_plugin'] = array(
'plugin' => 'raw_url',
'url_method' => LINKIT_URL_METHOD_RAW,
);
}
// Save the profile.
ctools_export_crud_save('linkit_profiles', $profile);
// Load and return the saved profile.
$this->_profile = linkit_profile_load($profile->name);
}
/**
* Helper function: Update a Linkit profile.
*/
protected function updateProfile() {
ctools_include('export');
// Save the changes to the profile.
ctools_export_crud_save('linkit_profiles', $this->_profile);
// Load the saved profile.
$this->_profile = linkit_profile_load($this->_profile->name);
}
/**
* Helper method to process the autocomplete call.
*/
protected function autocompleteCall() {
// Call the autocomplete.
$path = 'linkit/autocomplete/' . $this->_profile->name;
$response = $this->drupalGetAJAX($path, array('query' => array(LINKIT_BAC_QUERY_KEY => $this->search_string)));
$this->assertResponse(200);
$this->assertNotNull($response, 'The JSON respone is seems to be ok.');
return $response;
}
}
@@ -0,0 +1,185 @@
<?php
/**
* @file
* Tests for Linkit module.
*/
/**
* Tests Linkit profile UI CRUD.
*/
class LinkitProfileUICRUDTestCase extends LinkitTestCase {
/**
* Default profile values.
*/
protected $profile_defaults = array(
'name' => 'test_profile',
'admin_title' => 'Test Profile',
'admin_description' => 'This is a description for the Test Profile.',
'profile_type' => LINKIT_PROFILE_TYPE_EDITOR,
);
/**
* Definition.
*/
public static function getInfo() {
return array(
'name' => 'Linkit Profile',
'description' => 'Test that a Linkit profile can be created/read(load)/updated/deleted.',
'group' => 'Linkit'
);
}
function setUp($extra_modules = array()) {
parent::setUp($extra_modules);
}
/**
* Create a profile.
*/
public function testCreateProfile() {
// Create a user and login.
$this->account = $this->drupalCreateUser($this->admin_permissions);
$this->drupalLogin($this->account);
// Save the profile.
$this->drupalPost('admin/config/content/linkit/add', $this->profile_defaults, t('Save'));
$this->assertRaw(t('!title has been created.', array('!title' => $this->profile_defaults['name'])), 'The new profile was created.');
// Go to the edit page.
$this->drupalGet('admin/config/content/linkit/list/' . $this->profile_defaults['name'] . '/edit');
$this->assertResponse(200);
// Test that the given values are saved.
foreach ($this->profile_defaults as $form_key => $value) {
$this->assertFieldByName($form_key, $value);
}
}
/**
* Update a profile.
*/
public function testUpdateProfile() {
// Create a user and login.
$this->account = $this->drupalCreateUser($this->admin_permissions);
$this->drupalLogin($this->account);
// Create a profile.
$this->createProfile(array(
'data' => array(
'autocomplete' => array(
'charLimit' => LINKIT_CHAR_LIMIT,
'wait' => LINKIT_WAIT,
'remoteTimeout' => LINKIT_REMOTE_TIMEOUT,
),
),
));
// Go to the edit page.
$this->drupalGet('admin/config/content/linkit/list/' . $this->_profile->name . '/edit');
$this->assertResponse(200);
$profile_update = array(
'admin_title' => 'Test Profile updated',
'admin_description' => 'This is a updated description for the Test Profile updated.',
'data[autocomplete][charLimit]' => 4,
'data[autocomplete][wait]' => 3500,
'data[autocomplete][remoteTimeout]' => 1000,
);
// Set some new values for the profile.
$this->drupalPost(NULL, $profile_update, t('Save'));
// Go to the edit page again.
$this->drupalGet('admin/config/content/linkit/list/' . $this->_profile->name . '/edit');
$this->assertResponse(200);
// Test that the given values are saved.
foreach ($profile_update as $form_key => $value) {
$this->assertFieldByName($form_key, $value);
}
}
/**
* Delete a profile.
*/
public function testDeleteProfile() {
// Create a user and login.
$this->account = $this->drupalCreateUser($this->admin_permissions);
$this->drupalLogin($this->account);
// Create a profile that we will delete.
$this->createProfile();
// Delete the created profile.
$this->drupalPost('/admin/config/content/linkit/list/' . $this->_profile->name . '/delete', array(), t('Delete'));
$this->assertRaw(t('The item has been deleted.'), 'The profile was deleted.');
}
/**
* Load a profile that doesn't exists.
*/
public function testLoadNonExistingProfile() {
// Load a profile that doesn't exists.
$loaded_profile = linkit_profile_load('my_none_existing_profile');
$this->assertFalse($loaded_profile, 'FALSE is returned when loading a non existing profile.');
}
/**
* Load a profile that exists.
*/
public function testLoadProfile() {
// Create an user account so we can create a profile.
$this->account = $this->drupalCreateUser($this->admin_permissions);
$this->drupalLogin($this->account);
// Create a profile that we will load.
$this->createProfile();
// Load the saved profile.
$loaded_profile = linkit_profile_load($this->_profile->name);
$this->assertTrue($loaded_profile, 'Profile was successfully loaded.');
}
/**
* Load all profiles when no profiles exists.
*/
public function testLoadProfilesNoProfilesExists() {
// We have to clear the static variables here as we have in the
// testLoadProfilesNoProfilesExists test function asked for profiles where
// there were no profiles.
drupal_static_reset('ctools_export_load_object');
drupal_static_reset('ctools_export_load_object_all');
// Load all profiles.
$loaded_profile = linkit_profile_load_all();
$this->assertIdentical($loaded_profile, array(), 'Empty array is returned when loading all profiles and there are no profiles.');
}
/**
* Load all profiles.
*/
public function testLoadAllProfiles() {
// Create an user account so we can create profiles.
$this->account = $this->drupalCreateUser($this->admin_permissions);
$this->drupalLogin($this->account);
// Create profiles that we will load.
$this->createProfile(array('name' => 'test_1'));
$this->createProfile(array('name' => 'test_2'));
$this->createProfile(array('name' => 'test_3'));
// We have to clear the static variables here as we have in the
// testLoadProfilesNoProfilesExists test function asked for profiles where
// there were no profiles.
drupal_static_reset('ctools_export_load_object');
drupal_static_reset('ctools_export_load_object_all');
// Load all profiles.
$loaded_profiles = linkit_profile_load_all();
$this->assertEqual(count($loaded_profiles), 3, 'All profile were successfully loaded.');
}
}
@@ -0,0 +1,17 @@
<?php
/**
* @file
* Tests for Linkit module.
*/
/**
* Search plugin test helper class.
*/
abstract class LinkitsearchPluginTestCase extends LinkitTestCase {
/**
* The string to search for when doing autocomplete calls.
*
* @var string
*/
protected $search_string = 'lorem';
}
@@ -0,0 +1,174 @@
<?php
/**
* @file
* Tests for Linkit search plugin node.
*/
/**
* Test the the node search plugin.
*/
class LinkitsearchPluginNodeTestCase extends LinkitsearchPluginTestCase {
/**
* Definition.
*/
public static function getInfo() {
return array(
'name' => 'Linkit Search plugin (Node)',
'description' => 'Test the the node search plugin.',
'group' => 'Linkit'
);
}
function setUp($extra_modules = array()) {
parent::setUp($extra_modules);
// Create a basic profile.
$this->createProfile(array(
'data' => array(
'search_plugins' => array(
'entity:node' => array(
'enabled' => 1,
'weight' => 0,
),
),
'entity:node' => array(
'result_description' => '',
'bundles' => array(),
'group_by_bundle' => 0,
'include_unpublished' => 0,
),
'insert_plugin' => array(
'url_method' => LINKIT_URL_METHOD_RAW,
),
)
));
}
/**
* Test that we get results back which is valid.
*/
public function testBasicResults() {
// Create some nodes.
$node_1 = $this->drupalCreateNode(array('type' => 'article', 'title' => $this->search_string . $this->randomName()));
$node_2 = $this->drupalCreateNode(array('type' => 'article', 'title' => $this->search_string . $this->randomName()));
$node_3 = $this->drupalCreateNode(array('type' => 'page', 'title' => $this->search_string . $this->randomName()));
$node_4 = $this->drupalCreateNode(array('type' => 'page', 'title' => $this->search_string . $this->randomName()));
// Call the autocomplete helper method.
$this->autocompleteCall();
// Assert that the node titles appears in the response.
$this->assertRaw($node_1->title, 'Node was found in the result array.');
$this->assertRaw($node_2->title, 'Node was found in the result array.');
$this->assertRaw($node_3->title, 'Node was found in the result array.');
$this->assertRaw($node_4->title, 'Node was found in the result array.');
}
/**
* Test how node states are handled.
*/
public function testUnpublishedItems() {
// Create some nodes.
$node_1 = $this->drupalCreateNode(array('type' => 'article', 'title' => $this->search_string . $this->randomName(), 'status' => NODE_NOT_PUBLISHED));
$node_2 = $this->drupalCreateNode(array('type' => 'page', 'title' => $this->search_string . $this->randomName(), 'status' => NODE_NOT_PUBLISHED));
// Call the autocomplete helper method.
$this->autocompleteCall();
// Assert that the unpublished node title doesn't appear in the response.
$this->assertNoRaw($node_1->title, 'Unpublished node was not found in the result array.');
$this->assertNoRaw($node_2->title, 'Unpublished node was not found in the result array.');
// Tell the profile to include unpublished nodes.
$this->_profile->data['entity:node']['include_unpublished'] = 1;
$this->updateProfile();
// In order to see unpublished nodes in the result, the user must also have
// permission to see unpublished nodes.
// We give the user 'bypass node access' permission as we don't crated the
// nodes with this user.
$this->account = $this->drupalCreateUser(array('bypass node access'));
$this->drupalLogin($this->account);
// Call the autocomplete helper method.
$this->autocompleteCall();
// Assert that the unpublished title appears in the response.
$this->assertRaw($node_1->title, 'Unpublished node was found in the result array.');
$this->assertRaw($node_2->title, 'Unpublished node was found in the result array.');
}
/**
* Test group by bundle.
*
* The actual grouping is made by the javascript later on.
* We just test that the resons contains group info.
*/
public function testGroupbyBundle() {
// Create some nodes.
$node_1 = $this->drupalCreateNode(array('type' => 'article', 'title' => $this->search_string . $this->randomName()));
$node_2 = $this->drupalCreateNode(array('type' => 'page', 'title' => $this->search_string . $this->randomName()));
// Get the bundle names.
$node_type_name_1 = node_type_get_name($node_1);
$node_type_name_2 = node_type_get_name($node_2);
// Update the profile to group by bundle.
$this->_profile->data['entity:node']['group_by_bundle'] = 1;
$this->updateProfile();
// Call the autocomplete helper method.
$this->autocompleteCall();
// Assert that the bundle title appear in the response.
$this->assertRaw('"group":"Node - ' . $node_type_name_1, 'Bundle name in group was found in the result array.');
$this->assertRaw('"group":"Node - ' . $node_type_name_2, 'Bundle name in group was found in the result array.');
}
/**
* Test bundle filter.
*/
public function testBundleFilter() {
// Create some nodes.
$node_1 = $this->drupalCreateNode(array('type' => 'article', 'title' => $this->search_string . $this->randomName()));
$node_2 = $this->drupalCreateNode(array('type' => 'page', 'title' => $this->search_string . $this->randomName()));
// Update the profile to filter by bundle.
$this->_profile->data['entity:node']['bundles'] = array(
'article' => 'article',
);
$this->updateProfile();
// Call the autocomplete helper method.
$res = $this->autocompleteCall();
// Assert that the node title from the bundle included in the filter appear in the response.
$this->assertRaw($node_1->title, 'Node that is included in the filter was found in the result array.');
// Assert that the node title from the bundle that is not included in the filter doesnät appear in the response.
$this->assertNoRaw($node_2->title, 'Node that is not included in the filter was not found in the result array.');
}
/**
* Test result description.
*
* We just test one token.
*/
public function testDescription() {
// Create some nodes.
$node_1 = $this->drupalCreateNode(array('type' => 'article', 'title' => $this->search_string . $this->randomName()));
$node_2 = $this->drupalCreateNode(array('type' => 'page', 'title' => $this->search_string . $this->randomName()));
// Update the profile with a user result description.
$this->_profile->data['entity:node']['result_description'] = 'Created [node:created:raw]';
$this->updateProfile();
// Call the autocomplete helper method.
$this->autocompleteCall();
// Check that the result description appers in the result.
$this->assertRaw('Created ' . $node_1->created, 'The result description was found in the result array.');
$this->assertRaw('Created ' . $node_2->created, 'The result description was found in the result array.');
}
}
@@ -0,0 +1,180 @@
<?php
/**
* @file
* Tests for Linkit search plugin taxonomy term.
*/
/**
* Test the the taxonomy term search plugin.
*/
class LinkitsearchPluginTaxonomyTermTestCase extends LinkitsearchPluginTestCase {
/**
* Definition.
*/
public static function getInfo() {
return array(
'name' => 'Linkit Search plugin (Taxonomy term)',
'description' => 'Test the the taxonomy term search plugin.',
'group' => 'Linkit'
);
}
function setUp($extra_modules = array()) {
parent::setUp(array('taxonomy'));
// Create a basic profile.
$this->createProfile(array(
'data' => array(
'search_plugins' => array(
'entity:taxonomy_term' => array(
'enabled' => 1,
'weight' => 0,
),
),
'entity:taxonomy_term' => array(
'result_description' => '',
'bundles' => array(),
'group_by_bundle' => 0,
),
'insert_plugin' => array(
'url_method' => LINKIT_URL_METHOD_RAW,
),
)
));
}
/**
* Create and returns a new vocabulary.
*/
function createVocabulary() {
// Create a vocabulary.
$vocabulary = new stdClass();
$vocabulary->name = $this->randomName();
$vocabulary->description = $this->randomName();
$vocabulary->machine_name = drupal_strtolower($this->randomName());
$vocabulary->help = '';
$vocabulary->nodes = array('article' => 'article');
$vocabulary->weight = mt_rand(0, 10);
taxonomy_vocabulary_save($vocabulary);
return $vocabulary;
}
/**
* Create and returns a new term in vocabulary $vid.
*/
function createTerm($vocabulary) {
$term = new stdClass();
$term->name = $this->search_string . $this->randomName();
$term->description = $this->randomName();
// Use the first available text format.
$term->format = 'plain_text';
$term->vid = $vocabulary->vid;
taxonomy_term_save($term);
return $term;
}
/**
* Test that we get results back which is valid.
*/
public function testBasicResults() {
// Create demo vocabulary.
$vocabulary = $this->createVocabulary();
// Create demo terms.
$term_1 = $this->createTerm($vocabulary);
$term_2 = $this->createTerm($vocabulary);
$term_3 = $this->createTerm($vocabulary);
// Call the autocomplete helper method.
$this->autocompleteCall();
// Check that the term names appears in the response.
$this->assertRaw($term_1->name, 'Term was found in the result array.');
$this->assertRaw($term_2->name, 'Term was found in the result array.');
$this->assertRaw($term_3->name, 'Term was found in the result array.');
}
/**
* Test group by bundle.
*
* The actual grouping is made by the javascript later on.
* We just test that the resons contains group info.
*/
public function testGroupbyBundle() {
// Create demo vocabulary.
$vocabulary_1 = $this->createVocabulary();
$vocabulary_2 = $this->createVocabulary();
// Create demo terms.
$term_1 = $this->createTerm($vocabulary_1);
$term_2 = $this->createTerm($vocabulary_1);
$term_3 = $this->createTerm($vocabulary_2);
$term_4 = $this->createTerm($vocabulary_2);
// Update the profile to group by bundle.
$this->_profile->data['entity:taxonomy_term']['group_by_bundle'] = 1;
$this->updateProfile();
// Call the autocomplete helper method.
$this->autocompleteCall();
// Assert that the bundle title appear in the response.
$this->assertRaw('"group":"Taxonomy term - ' . $vocabulary_1->name, 'Bundle name in group was found in the result array.');
$this->assertRaw('"group":"Taxonomy term - ' . $vocabulary_1->name, 'Bundle name in group was found in the result array.');
}
/**
* Test bundle filter.
*/
public function testBundleFilter() {
// Create demo vocabulary.
$vocabulary_1 = $this->createVocabulary();
$vocabulary_2 = $this->createVocabulary();
// Create demo terms.
$term_1 = $this->createTerm($vocabulary_1);
$term_2 = $this->createTerm($vocabulary_2);
// Update the profile to filter by bundle.
$this->_profile->data['entity:taxonomy_term']['bundles'] = array(
$vocabulary_1->machine_name => $vocabulary_1->machine_name,
);
$this->updateProfile();
// Call the autocomplete helper method.
$this->autocompleteCall();
// Assert that the term title from the bundle included in the filter appear in the response.
$this->assertRaw($term_1->name, 'Term that is included in the filter was found in the result array.');
// Assert that the term title from the bundle that is not included in the filter doesnät appear in the response.
$this->assertNoRaw($term_2->name, 'Term that is not included in the filter was not found in the result array.');
}
/**
* Test result description.
*
* We just test one token.
*/
public function testDescription() {
// Create demo vocabulary.
$vocabulary_1 = $this->createVocabulary();
$vocabulary_2 = $this->createVocabulary();
// Create demo terms.
$term_1 = $this->createTerm($vocabulary_1);
$term_2 = $this->createTerm($vocabulary_2);
// Update the profile with a taxonomy_term result description.
$this->_profile->data['entity:taxonomy_term']['result_description'] = 'Term [term:vocabulary]';
$this->updateProfile();
// Call the autocomplete helper method.
$this->autocompleteCall();
// Check that the result description appers in the result.
$this->assertRaw('Term ' . $vocabulary_1->name, 'The result description was found in the result array.');
$this->assertRaw('Term ' . $vocabulary_2->name, 'The result description was found in the result array.');
}
}
@@ -0,0 +1,117 @@
<?php
/**
* @file
* Tests for Linkit search plugin user.
*/
/**
* Test the the user search plugin.
*/
class LinkitsearchPluginUserTestCase extends LinkitsearchPluginTestCase {
/**
* Definition.
*/
public static function getInfo() {
return array(
'name' => 'Linkit Search plugin (User)',
'description' => 'Test the the user search plugin.',
'group' => 'Linkit'
);
}
function setUp($extra_modules = array()) {
parent::setUp($extra_modules);
// Create a basic profile.
$this->createProfile(array(
'data' => array(
'search_plugins' => array(
'entity:user' => array(
'enabled' => 1,
'weight' => 0,
),
),
'entity:user' => array(
'result_description' => '',
),
'insert_plugin' => array(
'url_method' => LINKIT_URL_METHOD_RAW,
),
)
));
// Users need the 'access user profiles' permission to search for users.
$this->account = $this->drupalCreateUser(array('access user profiles'));
$this->drupalLogin($this->account);
}
/**
* Create a user.
*
* We can't use drupalCreateUser() as we want to set specific usernames.
*/
protected function createUser($extra = array()) {
// Create a user assigned to that role.
$edit = array();
$edit += $extra;
if (!isset($edit['name'])) {
$edit['name'] = $this->randomName();
}
$edit['mail'] = $edit['name'] . '@example.com';
$edit['pass'] = user_password();
$edit['status'] = 1;
$account = user_save(drupal_anonymous_user(), $edit);
$this->assertTrue(!empty($account->uid), t('User created with name %name and pass %pass', array('%name' => $edit['name'], '%pass' => $edit['pass'])), t('User login'));
if (empty($account->uid)) {
return FALSE;
}
// Add the raw password so that we can log in as this user.
$account->pass_raw = $edit['pass'];
return $account;
}
/**
* Test that we get results back which is valid.
*/
public function testBasicResults() {
// We don't use the drupalCreateUser() as we want to set specific usernames.
$user_1 = $this->createUser(array('name' => $this->search_string . $this->randomName()));
$user_2 = $this->createUser(array('name' => $this->search_string . $this->randomName()));
$user_3 = $this->createUser(array('name' => $this->search_string . $this->randomName()));
// Call the autocomplete helper method.
$this->autocompleteCall();
// Check that the usernames appears in the response.
$this->assertRaw($user_1->name, 'Username was found in the result array.');
$this->assertRaw($user_2->name, 'Username was found in the result array.');
$this->assertRaw($user_3->name, 'Username was found in the result array.');
}
/**
* Test result description.
*
* We just test one token.
*/
public function testDescription() {
// We don't use the drupalCreateUser() as we want to set specific usernames.
$user_1 = $this->createUser(array('name' => $this->search_string . $this->randomName()));
$user_2 = $this->createUser(array('name' => $this->search_string . $this->randomName()));
// Update the profile with a user result description.
$this->_profile->data['entity:user']['result_description'] = 'Created [user:created:raw]';
$this->updateProfile();
// Call the autocomplete helper method.
$this->autocompleteCall();
// Check that the result description appers in the result.
$this->assertRaw('Created ' . $user_1->created, 'The result description was found in the result array.');
$this->assertRaw('Created ' . $user_2->created, 'The result description was found in the result array.');
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB