/*
* Gallery - a web based photo album viewer and editor
* Copyright (C) 2000-2008 Bharat Mediratta
*
* 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.
*/
/**
* Gallery2 ImageAreas DHTML JavaScript
* Provides Javascript UI for selecting areas of images and interacts with the ImageAreas module on
* the server side.
*
* Adapted From: Fotonotes DHTML Client (c) 2004-2005 Angus Turnbull http://www.twinhelix.com
* Developed under license to FotoNotes LLC
* Released under the Open Source License v2.1 or later.
*
* @package ImageAreas
* @author Paul Hinze <paul.t.hinze@gmail.com>
* @version $Revision$
*/
/**************************************
* CONFIGURATION, VARIABLES AND SETUP *
**************************************/
// Address of server (to be set by template)
var iaServer = null;
// Item on page (to be set by template)
var itemId = null;
// XMLHTTPRequest object to communicate with server.
var iaXMLHTTP = null;
if (window.ActiveXObject) {
try {
iaXMLHTTP = new ActiveXObject('Microsoft.XMLHTTP');
}
catch (e) { }
}
else if (window.XMLHttpRequest) {
iaXMLHTTP = new XMLHttpRequest();
}
// Permissions (respect previous settings):
// Allowed values are 'allow', 'prompt', 'deny'.
if (!window.IA_ADD) var IA_ADD = 'allow';
if (!window.IA_MODIFY) var IA_MODIFY = 'allow';
if (!window.IA_DELETE) var IA_DELETE = 'allow';
// Internationalisation:
var IA_CREDITS = 'ImageAreas Module UI\n\n' +
'Based on Fotonotes DHTML viewer, (c) 2004-2005 Angus Turnbull, http://www.twinhelix.com\n\n';
var IA_DISALLOWED = 'Sorry, that action is not permitted.\n\n' +
'Please login under a different account.';
var IA_POST_UNSUPPORTED = 'Sorry, your browser does not support editing notes.';
var IA_DELETE_CONFIRM = 'Are you sure you want to delete this the following note?';
var IA_SAVE_WAIT = 'Loading ImageAreas module...';
var IA_SAVE_FAIL = 'An error occurred, and your changes could not be saved.';
var IA_LOAD_FAIL = 'An error occurred, and the ImageAreas module could not load.';
var IA_SAVE_FAIL_JPEG_NOT_WRITABLE = "JPEG file is not writable." +
"Please check file permissions on server.";
var IA_SAVE_SUCCESS = 'Changes saved!';
var IA_MOVED_LINK = 'View image';
// Other global variables:
var iaDebugMode = false; // Set to true to show XML sent/received.
var iaHideTimer = null; // Hide notes after timeout.
var iaActiveNote = null; // Currently visible note.
var iaActionVerb = ''; // Control bar's current action.
var iaActionTrigger = null; // Control bar's lit item.
var iaEditingData = null; // Data store during note editing process.
var iaMinImgWidth = 300; // MinWidth to make to apply to ia-image
var iaMinImgHeight = 300; // MinHeight to make to apply to ia-image
/**
* Finds the image(s) in the DOM for which Fotonotes should be activated and does so.
* Currently applies to all images greater than minimum height/width.
* TODO: specifically find the main image on photo.tpl in the DOM
* @param url url of server as generated by template
* @param id id of image we're loading for
*/
function loadImageAreas(url, id) {
iaServer = url;
itemId = id;
for (i=0;i < document.images.length; i++) {
if ( (document.images[i].width >= iaMinImgWidth)
&& (document.images[i].height >= iaMinImgHeight)) {
var imgObj = document.images[i];
moveLinkBelow(imgObj, IA_MOVED_LINK);
getImageAreasUI(imgObj);
}
}
}
/**
* If the element is inside a link, remove it and place the link below the element.
* @param elm DOM element to remove from link
* @param linkText text to use for the moved link
*/
function moveLinkBelow(elm, linkText) {
if (elm.parentNode.tagName == "A") {
var linkNode = elm.parentNode;
var origParent = linkNode.parentNode;
origParent.replaceChild(elm, linkNode);
linkNode.innerHTML = linkText;
origParent.appendChild(linkNode);
}
}
/**
* Sends some XML off to the server to get UI for image; calls iaGetClientComplete on
* completion.
* @param imgObj reference to <img> element in the DOM
*/
function getImageAreasUI(imgObj) {
var imageFile = imgObj.src;
// Compose our post content and send it.
var postContent = 'g2_src=' + escape(imageFile) + '&g2_action=' + 'display'
+ '&g2_itemId=' + itemId + '&g2_width=' + imgObj.width + '&g2_height=' + imgObj.height
+ '&g2_alt=' + imgObj.alt;
iaXMLHTTP.open('POST', iaServer, true);
iaXMLHTTP.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=utf-8');
iaXMLHTTP.setRequestHeader('Content-length', postContent.length);
iaXMLHTTP.onreadystatechange = function() {
if (iaXMLHTTP.readyState == 4) receiveImageAreasUI(true,imgObj,iaXMLHTTP.responseText);
};
iaModalDialog(IA_SAVE_WAIT);
iaXMLHTTP.send(postContent);
}
/**
* All successful actions: let the user know it's OK, and reset the control bar,
* and clear the editing data store.
*/
function receiveImageAreasUI(ok,imgObj,responseText) {
setTimeout('iaModalDialog("")', 500);
// Called once the server responds post-Save operation. 'ok' indicates success.
// EXTRACT returned HTML text from reply to update document
re = /displayHTML##([\w\W\n\r]*)##/;
matches = re.exec(responseText);
if(!matches || matches.length < 2) {
/* Something bad happened on the server side. We lose. */
return;
}
var iaDiv = matches[1]; // first matched pattern
iaDivElement = document.createElement('div');
iaDivElement.innerHTML = iaDiv;
imgObj.parentNode.insertBefore(iaDivElement,imgObj);
imgObj.parentNode.removeChild(imgObj);
}
/*************
* CORE CODE *
*************/
/**
* Fader function that shows/hides an element.
* @param elm DOM element to show or hide
* @param show true to fade in, false to fade out
*/
function iaElementFade(elm, show) {
var speed = show ? 20 : 10;
elm._f_count |= 0;
elm._f_timer |= null;
clearTimeout(elm._f_timer);
if (show && !elm._f_count) elm.style.visibility = 'inherit';
elm._f_count = Math.max(0, Math.min(100, elm._f_count + speed*(show?1:-1)));
var f = elm.filters, done = (elm._f_count==100);
if (f) {
if (!done && elm.style.filter.indexOf("alpha") == -1) {
elm.style.filter += ' alpha(opacity=' + elm._f_count + ')';
}
else if (f.length && f.alpha) {
with (f.alpha) {
if (done) enabled = false;
else { opacity = elm._f_count; enabled=true }
}
}
}
else {
elm.style.opacity = elm.style.MozOpacity = elm._f_count/100.1;
}
if (!show && !elm._f_count) { elm.style.visibility = 'hidden'; }
if (elm._f_count % 100) {
elm._f_timer = setTimeout(function() { iaElementFade(elm,show) }, 50);
}
}
/**
* Utility function that toggles the "-active" and "-inactive" classnames.
* @param elm DOM element to toggle
* @param active true for active, false for inactive
*/
function iaClassSet(elm, active) {
elm.className = elm.className.replace((active ? (/-inactive/) : (/-active/)),
(active ? '-active' : '-inactive'));
}
/**
* When passed a DOM node, returns its parent "ia-container".
* @param node DOM node
*/
function iaGetContainer(node) {
var container = node;
while (container) {
if ((/ia-container/).test(container.className)) { break; }
container = container.parentNode;
}
return container;
}
/**
* When passed a container, returns the control bar within that container.
* @param container DOM reference to a container
*/
function iaGetControlBar(container) {
var controlBar = null;
for (var i = 0; i < container.childNodes.length; i++) {
if ((/ia-controlbar/).test(container.childNodes.item(i).className)) {
controlBar = container.childNodes.item(i);
break;
}
}
return controlBar;
};
/**
* Sets the "activated" status of a note container area, and changes
* the appropriate "toggle" item in its control bar.
*/
function iaContainerSet(container, active) {
var controlBar = iaGetControlBar(container);
for (var i = 0; i < controlBar.childNodes.length; i++) {
if ((/ia-controlbar-toggle/).test(controlBar.childNodes.item(i).className)) {
iaClassSet(controlBar.childNodes.item(i), !active);
break;
}
}
iaClassSet(container, active);
}
/**
* Called on click of control buttons to highlight/dim them.
*/
function iaAction(action, trigger) {
// Control the state of the trigger buttons, and set the global iaActionVerb variable.
if (iaActionVerb != action) {
// Set a new action, dim the old button.
if (iaActionTrigger && iaActionVerb) { iaClassSet(iaActionTrigger, false); }
iaActionVerb = action;
iaActionTrigger = trigger;
if (trigger) { iaClassSet(trigger, true); }
}
else {
// Deactivate a trigger that is clicked twice.
iaActionVerb = '';
if (trigger) { iaClassSet(trigger, false); }
}
}
/**
* Called on document.onmouseover & onmouseout, manages tip visibility.
*/
function iaMouseOverOutHandler(evt, isOver) {
var node = evt.target || evt.srcElement;
if (node.nodeType != 1) node = node.parentNode;
while (node && !((node.className||'').indexOf('ia-container') > -1)) {
// If the node has an CLASS of "fotonote-area", process it.
// No mouseovers if iaActionVerb is set (i.e. editing/deleting/adding/etc).
if (node && ((node.className||'').indexOf('ia-area') > -1) && !iaActionVerb) {
var area = node;
// Find the first child element, which will be the note in question.
var note = area.firstChild;
while (note && note.nodeType != 1) note = note.nextSibling;
if (!note) return;
// Clear any hide timeout, and either show the note, or set a timeout for its hide.
// We record the currently active note for the hide timer to work, and also elevate
// its parent area above any previously active area (which is lowered).
clearTimeout(iaHideTimer);
if (isOver) {
if (iaActiveNote && (note != iaActiveNote)) { iaElementFade(iaActiveNote, false); }
iaElementFade(note, true);
if (iaActiveNote) { iaActiveNote.parentNode.style.zIndex = 1; }
note.parentNode.style.zIndex = 2;
iaActiveNote = note;
}
else {
iaHideTimer = setTimeout('if (iaActiveNote) { ' +
'iaElementFade(iaActiveNote, false); iaActiveNote = null }', 200);
}
}
// Loop up the DOM.
node = node.parentNode;
}
}
/**
* Processes clicks on the document, performs the correct action.
* @param evt event
*/
function iaClickHandler(evt) {
var node = evt.target || evt.srcElement;
if (node.nodeType != 1) node = node.parentNode;
while (node && !((node.className||'').indexOf('ia-container') > -1)) {
// Check buttons within the Edit bar.
if ((/ia-editbar-ok/).test(node.className)) { return iaEditButtonHandler(true); }
if ((/ia-editbar-cancel/).test(node.className)) { return iaEditButtonHandler(false); }
// Perform no other if we're currently editing a note.
if (iaEditingData) { return; }
// If an existing area with a CLASS of the form "ia-area"
// has been clicked, check if we're editing/deleting it.
if ((/ia-area/).test(node.className)) {
var area = node;
if (iaActionVerb == 'del') { iaDelNote(area); }
if (iaActionVerb == 'edit') {
var note = area.firstChild;
while (note && note.nodeType != 1) note = note.nextSibling;
if (note) iaEditNote(note);
}
return;
}
// Buttons on/within the Control bar.
if ((/ia-controlbar-logo/).test(node.className)) {
// Logo click toggles control bar, if we're not editing a note.
var isActive = ((/ia-controlbar-active/).test(node.parentNode.className));
iaClassSet(node.parentNode, !isActive);
return;
}
if ((/ia-controlbar-credits/).test(node.className)) {
alert(IA_CREDITS);
return;
}
if ((/ia-controlbar-del/).test(node.className)) {
if (!iaXMLHTTP) { return alert(IA_POST_UNSUPPORTED); }
if (IA_DELETE == 'deny') { return alert(IA_DISALLOWED); }
return iaAction('del', node);
}
if ((/ia-controlbar-edit/).test(node.className)) {
if (!iaXMLHTTP) { return alert(IA_POST_UNSUPPORTED); }
if (IA_MODIFY == 'deny') { return alert(IA_DISALLOWED); }
return iaAction('edit', node);
}
if ((/ia-controlbar-add/).test(node.className)) {
if (!iaXMLHTTP) { return alert(IA_POST_UNSUPPORTED); }
if (IA_ADD == 'deny') { return alert(IA_DISALLOWED); }
return iaAddNote(node);
}
if ((/ia-controlbar-toggle/).test(node.className)) {
// Find the parent container, and toggle its classname to show/hide notes.
var container = iaGetContainer(node);
if (container) {
var isActive = ((/ia-container-active/).test(container.className));
iaContainerSet(container, !isActive);
}
}
// Otherwise, loop up the hierarchy.
node = node.parentNode;
}
}
/*
* Either shows or hides the editing UI.
* @param show true to show, false to hide
*/
function iaEditUISet(show) {
if (!iaEditingData) return;
// Find our container and form references.
var container = iaGetContainer(area);
if (!container) { return; }
var form = container.getElementsByTagName('form');
if (!form) { return; }
form = form.item(0);
with (iaEditingData) {
// Start or stop dragging the selected area.
if (show) { dragresize.select(area, area); }
else { dragresize.deselect(true); }
// Set area className so its remains visible if editing, or reset it back otherwise.
area.className = show ? 'ia-area-editing' : 'ia-area';
// Fade the editing UI in/out, and toggle its classname so it stays that way.
iaElementFade(form, show);
iaClassSet(form, show);
// Toggle the container class and control bar (for other notes' visibility)
iaContainerSet(container, !show);
iaClassSet(iaGetControlBar(container), !show);
}
}
/**
* Adds a new note when the specified button is clicked.
* @param node
*/
function iaAddNote(node) {
// Find the parent container of this node.
var container = iaGetContainer(node);
if (!container) return;
// Highlight the "Add" button.
iaAction('add', node);
// Create a new area in which the note will reside.
var newArea = document.createElement('div');
newArea.className = 'ia-area';
newArea.style.left = (container.offsetWidth/2 - 25) + 'px';
newArea.style.top = (container.offsetHeight/2 - 25) + 'px';
newArea.style.width = '50px';
newArea.style.height = '50px';
newArea.className = newArea.className + ' ia-area-new';
var newNote = document.createElement('div');
newNote.className = 'ia-note';
newArea.appendChild(newNote);
// Create note elements.
var newTitle = document.createElement('span');
newTitle.className = 'ia-note-title';
newNote.appendChild(newTitle);
var newContent = document.createElement('span');
newContent.className = 'ia-note-content';
newNote.appendChild(newContent);
// add in innerborders
var newInnerBorder = document.createElement('div');
newInnerBorder.className = 'ia-area-innerborder-right';
newArea.appendChild(newInnerBorder);
var newInnerBorder = document.createElement('div');
newInnerBorder.className = 'ia-area-innerborder-left';
newArea.appendChild(newInnerBorder);
var newInnerBorder = document.createElement('div');
newInnerBorder.className = 'ia-area-innerborder-top';
newArea.appendChild(newInnerBorder);
var newInnerBorder = document.createElement('div');
newInnerBorder.className = 'ia-area-innerborder-bottom';
newArea.appendChild(newInnerBorder);
// Add newArea to document
container.appendChild(newArea);
// Record this note as editing, and set the "add" action flag.
iaEditingData = {
area: newArea,
note: newNote
};
// Hand over to the editing function.
iaEditNote();
}
/**
* Edits a passed note reference.
* @param note note to edit, if existing
*/
function iaEditNote(note)
{
var area = null;
if (note) {
// If we're editing an existing note, setup the data store.
area = note.parentNode;
iaEditingData = {
area: area,
note: note
};
}
else {
// New notes: pull the note and area out of the stored data.
area = iaEditingData.area;
note = iaEditingData.note;
}
// Find our container and form references.
var container = iaGetContainer(area);
if (!container) { return; }
var form = container.getElementsByTagName('form');
if (!form) { return; }
form = form.item(0);
// Pick up existing values for content from the note.
var oldTitle = '', oldAuthor = '', oldContent = '', noteID = '';
var fields = area.getElementsByTagName('span');
for (var n = 0; n < fields.length; n++) {
var field = fields.item(n);
if (field.className == 'ia-note-id') { noteID = field.getAttribute('title'); }
if (field.className == 'ia-note-title') { oldTitle = field.innerHTML; }
if (field.className == 'ia-note-author') { oldAuthor = field.innerHTML; }
if (field.className == 'ia-note-content') { oldContent = field.innerHTML; }
}
// Backup the original content, refs and position in our datastore.
// It already has the .note and .area properties.
// And yes, I know innerHTML isn't standard, but it's SO MUCH EASIER here!
iaEditingData.container = container;
iaEditingData.form = form;
iaEditingData.noteID = noteID;
iaEditingData.oldTitle = oldTitle;
iaEditingData.oldAuthor = oldAuthor;
iaEditingData.oldContent = oldContent;
iaEditingData.oldLeft = parseInt(area.style.left);
iaEditingData.oldTop = parseInt(area.style.top);
iaEditingData.oldWidth = area.offsetWidth;
iaEditingData.oldHeight = area.offsetHeight;
// Some values for the post-editing callback handler to populate.
iaEditingData.newTitle = iaEditingData.newAuthor = iaEditingData.newContent = '';
iaEditingData.newLeft = iaEditingData.newTop = 0;
iaEditingData.newWidth = iaEditingData.newHeight = 0;
// Populate the editing UI with its current content.
var inputs = form.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
if ((/title/).test(inputs[i].className)) inputs[i].value = oldTitle;
if ((/author/).test(inputs[i].className)) inputs[i].value = oldAuthor;
}
var textarea = form.getElementsByTagName('textarea');
if (textarea && (/content/).test(textarea.item(0).className)) {
textarea.item(0).value = oldContent;
}
// Finally, show the editing UI for the recorded area.
iaEditUISet(true);
}
/**
* Returns a properly escaped HTML string.
* @param html HTML string to escape
*/
function iaEscapeHTML(html) {
return html.replace('&', '&').replace('<', '<').replace('>', '>');
}
/**
* Button click handler from the editing UI.
* @param ok Pass a boolean value indicating if the OK button was clicked (so save should proceed).
*/
function iaEditButtonHandler(ok) {
if (!iaEditingData) { return; }
with (iaEditingData) {
if (ok) {
// Populate iaEditingData.new* from the edit form fields and area attributes.
// SET default value for all params.
newTitle = newAuthor = newUserid = newEntryid = newContent = newBorderColor = '';
var inputs = form.getElementsByTagName('input');
for (var i = 0; i < inputs.length; i++) {
if ((/title/).test(inputs[i].className)) {newTitle = inputs[i].value;}
if ((/author/).test(inputs[i].className)) {newAuthor = inputs[i].value;}
if ((/userid/).test(inputs[i].className)) {newUserid = inputs[i].value;}
if ((/entry_id/).test(inputs[i].className)) {newEntryid = inputs[i].value;}
if ((/border_color/).test(inputs[i].className)) {newBorderColor = inputs[i].value;}
}
var textarea = form.getElementsByTagName('textarea');
if (textarea && (/content/).test(textarea.item(0).className)) {newContent = textarea.item(0).value};
newLeft = parseInt(area.style.left);
newTop = parseInt(area.style.top);
newWidth = area.offsetWidth;
newHeight = area.offsetHeight;
if (iaDebugMode) {
alert('Begin server save operation ' + 'newBorderColor: ' + newBorderColor);
}
// Get the scalefactor from a hidden SPAN in the container.
var sFact = 1;
for (var n = 0; n < container.childNodes.length; n++) {
if ((/ia-scalefactor/).test(container.childNodes.item(n).className))
sFact = parseFloat(container.childNodes.item(n).getAttribute('title'));
}
// Begin server save operation.
var data = { 'areaId' : (iaActionVerb == 'edit' ? noteID : ''),
'itemId' : itemId,
'areaTitle' : iaEscapeHTML(newTitle),
'areaContent' : iaEscapeHTML(newContent),
'areaBounds' : parseInt(newLeft/sFact) + ','
+ parseInt(newTop/sFact) + ','
+ parseInt((newLeft+newWidth)/sFact) + ','
+ parseInt((newTop+newHeight)/sFact)
}
iaPostJSON(data);
}
else {
// For "cancel" clicks:
if (iaActionVerb == 'add') {
// Just delete new notes.
area.parentNode.removeChild(area);
}
else {
// Restore original note area position/size for edited notes.
area.style.left = oldLeft + 'px';
area.style.top = oldTop + 'px';
area.style.width = oldWidth + 'px';
area.style.height = oldHeight + 'px';
}
// Hide the editing UI, reset the control bar, clear the data store.
iaEditUISet(false);
iaAction('', null);
iaEditingData = null;
}
}
}
/**
* Deletes a note area
* @param area a whole area reference
*/
function iaDelNote(area)
{
// Find the ID of this note.
var areaID = iaGetAreaID(area);
if (!areaID) { alert(IA_SAVE_FAIL); }
area.className += ' ia-area-highlight';
var areaTitle = iaGetAreaTitle(area);
var areaContent = iaGetAreaContent(area);
if (areaID && confirm(IA_DELETE_CONFIRM + '\n\n' + areaTitle + '\n' + areaContent)) {
// Set up our data store to delete this area, and post to the server.
iaEditingData = {
area: area,
note: null,
container: iaGetContainer(area)
};
iaPostJSON( { 'areaId' : areaID } );
}
else {
// Reset control bar if cancelled.
area.className = 'ia-area';
iaAction('', null);
}
}
/**
* Given an area, returns the areaId data stored in the XHTML.
*/
function iaGetAreaID(area) {
fields = area.getElementsByTagName('span');
for (var n = 0; n < fields.length; n++) {
if (fields.item(n).className == 'ia-note-id') {
return fields.item(n).getAttribute('title');
}
}
return null;
}
/**
* Given an area, returns the title data stored in the XHTML.
*/
function iaGetAreaTitle(area) {
fields = area.getElementsByTagName('span');
for (var n = 0; n < fields.length; n++) {
if (fields.item(n).className == 'ia-note-title') {
return fields.item(n).innerHTML;
}
}
return null;
}
/**
* Given an area, returns the content data stored in the XHTML.
*/
function iaGetAreaContent(area) {
fields = area.getElementsByTagName('span');
for (var n = 0; n < fields.length; n++) {
if (fields.item(n).className == 'ia-note-content') {
return fields.item(n).innerHTML;
}
}
return null;
}
/**
* Shows or hides the browser-wide modal dialog.
* @param message Pass a message to show, or an empty string to hide the dialog.
*/
function iaModalDialog(message) {
var dialog = document.getElementById('ia-modaldialog');
if (!dialog) {
dialog = document.createElement('div');
dialog.setAttribute('id', 'ia-modaldialog');
document.body.appendChild(dialog);
}
dialog.innerHTML = '<span>' + message + '</span>';
dialog.style.visibility = message ? 'visible' : 'hidden';
}
/**
* Sends some JSON off to the server and calls iaEditComplete on completion.
* @param data object to send
*/
function iaPostJSON(data) {
// Hopefully my auto-detect-fu powers are strong. I'll use the Crouching Regex Style.
var image = iaEditingData.container.getElementsByTagName('img').item(0);
var imageFile = image.getAttribute('src');
if (!imageFile) return alert(IA_SAVE_FAIL);
// Compose our post content and send it.
var actVerbs = { add: 'add', edit: 'modify', del: 'delete' };