Source of “photos.js”.
1720 lines, 47.7 KBytes.   Last modified 5:21 pm, 1st May 2016 PDT.
1 // Emacs settings: -*- mode: Fundamental; tab-width: 4; -*- 2 3 //////////////////////////////////////////////////////////////////////////// 4 // // 5 // Andrew's Album Applications: photos.js // 6 // // 7 // Copyright (c) 2004-2016, Andrew Birrell // 8 // // 9 //////////////////////////////////////////////////////////////////////////// 10 11 "use strict"; 12 13 // 14 // Global variables 15 // 16 17 var autoInterval = 4000; // auto-play timer interval 18 19 // Various DOM elements, cached for code simplicity 20 // 21 var writing; // element for "writing" message 22 var editInner; // inside of edit dialog 23 var editTitle; // edit dialog title type-in element 24 var editThumb; // edit dialog use-as-thumb element 25 26 // Data structures 27 // 28 var btns = {}; // button details 29 var dlogs = {}; // pop-up dialogs 30 var swiper; // swiper.cur is the main content DIV 31 var cacheP; // cache of photo XML responses 32 var cacheF; // cache of folder XML responses 33 var cacheHits = 0; 34 var cacheMisses = 0; 35 36 // Mutable UI state 37 // 38 var thisPage = null; // attributes for current page 39 var user = ""; // default user for editing 40 var jumpTarget = null; // if set, switch to here instead 41 var timer = null; // auto-play timer, for cancelling 42 var autoRoot = null; // root directory for auto-play 43 var preloadServer = false; // variant of auto-play to preload server cache 44 var buttonsVisible = true; // visibility of the button bars 45 var titleVisible = true; // visibility of the title bar 46 var mainZoomLevel = 1; // zooming main image 47 var thumbShrinkLevel = 3; // shrinking photo thumbnails (in thirds) 48 var thumbDownscale = 1; // use smaller photo thumbnails on small screens 49 50 // Templates for dynamically generated HTML 51 // 52 var mainFrag = // Template HTML fragment for main photo itself 53 '<div class=mainPhoto onclick="return photoClick(event)">' + 54 '<div class=linePad></div>' + 55 '<img id=<#ID> src="<#SRC>" alt="">' + 56 '</div>'; 57 var preloadFrag = // Template HTML for preloading-server mode 58 "<div class=preloadServer>" + 59 "Preloading the server-side cache," + 60 " with no image display and no delays.<p>" + 61 "Click \"Stop\" to exit from this mode.<p>" + 62 "<#PATH><p>" + 63 "<#TITLE>" + 64 "</div>"; 65 var folderFrag = // Template HTML fragment for sub-folder listing 66 '<div class=subFolder>' + 67 '<div class=mini onclick="' + 68 "return jumpTo('<#PATH>')" + 69 '" title="Move to this folder">' + 70 '<div class=linePad></div>' + 71 '<img src="<#SRC>" alt=""></div>' + 72 '<div class=miniTitle onclick="' + 73 "return jumpTo('<#PATH>')" + 74 '" title="Move to this folder"><#TITLE></div>' + 75 '</div>'; 76 var photoFrag = // Template HTML fragment for photo listing 77 '<div class=listingPhoto id="<#ID>" title="<#TITLE>" ' + 78 'onclick="return jumpTo(\'<#PATH>\')" ' + 79 'draggable="true" ' + 80 'ondragstart="return thumbDragStart(event, this)">' + 81 '<div class=dropTargetL ' + 82 'style="height: <#HPAD>px; padding-right: <#LPAD>px" ' + 83 'ondragenter="return thumbDragEnter(event, this, true)">' + 84 '</div>' + 85 '<img width="<#WIDTH>" height="<#HEIGHT>" ' + 86 'src="<#SRC>" alt="" ' + 87 'style="margin-left: 0px; margin-right: 0px">' + 88 '<div class=dropTargetR ' + 89 'style="height: <#HPAD>px; padding-left: <#RPAD>px" ' + 90 'ondragenter="return thumbDragEnter(event, this, false)">' + 91 '</div>' + 92 '<#COMMENT>' + 93 '</div>'; 94 var photoPadFrag = // Template HTML for photo list padding 95 '<div class=listingPad style="width: <#WIDTH>px"></div>'; 96 97 98 // 99 // Dragging dialog boxes, just for fun 100 // 101 102 function dragStart(event, handle, id, highlightClass) { 103 // Called on mousedown on the drag element "handle". 104 // Sets things up to drag the element named by "id". 105 // During the drag, handle's class is replaced with highlightClass. 106 // 107 var elt = document.getElementById(id); 108 var startEltPos = adbab.getElementPos(elt); 109 var mousePos = adbab.getMousePos(event); 110 var offset = { 111 x: startEltPos.x - mousePos.x, 112 y: startEltPos.y - mousePos.y 113 }; 114 var savedClass = handle.className; 115 handle.className = highlightClass; 116 document.onmousemove = function dlogDragMove(event) { 117 var wSize = adbab.windowSize(); 118 var mousePos = adbab.getMousePos(event); 119 var newLeft = 120 Math.min(wSize.x - 32, Math.max(0, offset.x + mousePos.x)); 121 var newTop = 122 Math.min(wSize.y - 32, Math.max(0, offset.y + mousePos.y)); 123 elt.style.left = "" + newLeft + "px"; 124 elt.style.right = "auto"; 125 elt.style.top = "" + newTop + "px"; 126 return false; 127 } 128 document.onmouseup = function dlogDragStop(event) { 129 handle.className = savedClass; 130 document.onmousemove = null; 131 document.onmouseup = null; 132 return false; 133 }; 134 return false; 135 } 136 137 138 // 139 // Re-usable code for swiping, for devices with touch screens 140 // 141 142 function swipeInit(cur, prev, next, vertical) { 143 // Setup an event handler on the given elements to enable touch swipes 144 // that move to a different element. Returns an object containing the 145 // DOM elements for cur, prev, and next. The object is updated when 146 // swipes are completed. 147 // 148 // The swiper keeps out of the way whenever the current element can be 149 // natively scrolled in the chosen direction. 150 // 151 // The client should provide methods on the resulting object: 152 // 153 // "ok(topLeft)" is called when swiping, and returns a boolean saying 154 // whether there is indeed a useful data item to move to. If 155 // "topLeft" that would be the next data item, otherwise the 156 // previous one. Returning false adjusts the user feedback and 157 // prevents the "action" method from being called. 158 // 159 // "action(topLeft)" is called on a completed swipe. The elements 160 // in the object have been cycled: if "topleft" then "cur" 161 // has been moved to "prev", "next" has been moved to "cur", and 162 // the old "prev" has been moved to "next". 163 // 164 // We don't support the "webkit", "moz", and "o" variants of transform 165 // and transition, only the standards-track version. 166 167 var elts = { 168 cur: swipeInitOne(cur), 169 prev: swipeInitOne(prev), 170 next: swipeInitOne(next), 171 ok: function(topLeft) { return false; }, 172 action: function() { } 173 }; 174 175 var startClientPos; 176 var movedTo; 177 var moved; 178 179 function swipeInitOne(id) { 180 var elt = document.getElementById(id); 181 if (elt.addEventListener) { 182 elt.addEventListener('touchstart', swipeTouchStart, false); 183 } 184 return elt; 185 } 186 187 function swipeMoveOne(elt, pos, msecTxt) { 188 // Transform one photo to given position 189 // 190 elt.style.transitionDuration = msecTxt; 191 elt.style.transform = 192 "translate(" + pos.x + "px, " + pos.y + "px)"; 193 } 194 195 function swipeConstrain(pos) { 196 // constrain "pos" to the dimension in which we're swiping 197 // 198 if (vertical) pos.x = 0; else pos.y = 0; 199 } 200 201 function swipeCurSize() { 202 // Return the size of the "cur" element 203 // 204 return {x: elts.cur.clientWidth, y: elts.cur.clientHeight}; 205 } 206 207 function swipeEdge(which, delta) { 208 // Return canonical top-left position for (prev,cur,next) as 209 // which = (-1,0,1), adjusted by delta. 210 // 211 var pos = swipeCurSize(); 212 pos.x *= which; 213 pos.y *= which; 214 if (delta) { 215 pos.x += delta.x; 216 pos.y += delta.y; 217 } 218 swipeConstrain(pos); 219 return pos; 220 } 221 222 function swipeMoveTo(pos, msec) { 223 // Translate to given position, in given milliseconds 224 // 225 var msecText = (msec && msec > 0 ? msec : 0) + "ms"; 226 swipeMoveOne(elts.cur, pos, msecText); 227 swipeMoveOne(elts.prev, swipeEdge(-1, pos), msecText); 228 swipeMoveOne(elts.next, swipeEdge(1, pos), msecText); 229 } 230 231 function swipeTouchStart(event) { 232 if (event.targetTouches.length != 1) return false; 233 var theTouch = event.targetTouches[0]; 234 startClientPos = {x: theTouch.clientX, y: theTouch.clientY}; 235 movedTo = {x: 0, y: 0}; 236 moved = false; 237 elts.cur.addEventListener('touchmove', swipeTouchMove, false); 238 elts.cur.addEventListener('touchend', swipeTouchEnd, false); 239 return false; 240 } 241 242 function swipeCleanup() { 243 // Remove our event handlers 244 // 245 elts.cur.removeEventListener('touchmove', swipeTouchMove, false); 246 elts.cur.removeEventListener('touchend', swipeTouchEnd, false); 247 } 248 249 function swipeTouchMove(event) { 250 if (event.targetTouches.length != 1) return false; 251 var theTouch = event.targetTouches[0]; 252 var pos = { 253 x: theTouch.clientX - startClientPos.x, 254 y: theTouch.clientY - startClientPos.y 255 }; 256 if (!moved && (vertical ? 257 Math.abs(pos.x) > Math.abs(pos.y) : 258 Math.abs(pos.y) > Math.abs(pos.x))) { 259 // Abandon the touch and leave it to the default. This enables 260 // native scrolling in the non-swiper axis. 261 swipeCleanup(); 262 return true; 263 } 264 swipeConstrain(pos); 265 var topLeft = pos.x + pos.y < 0; 266 var scrollSize = (vertical ? 267 elts.cur.scrollHeight - elts.cur.clientHeight : 268 elts.cur.scrollWidth - elts.cur.clientWidth); 269 var scrollPos = (vertical ? 270 elts.cur.scrollTop : 271 elts.cur.scrollLeft); 272 var disabled = (topLeft ? scrollPos < scrollSize : scrollPos > 0); 273 if (disabled) { // Step aside to allow native scrolling of "cur" 274 swipeCleanup(); 275 return true; 276 } 277 if (!elts.ok(topLeft)) { // no prev/next item, so slip a little 278 pos.x = Math.round(pos.x / 3); 279 pos.y = Math.round(pos.y / 3); 280 } 281 elts.prev.style.display = "block"; 282 elts.next.style.display = "block"; 283 swipeMoveTo(pos, 0); 284 movedTo = pos; 285 moved = true; 286 event.preventDefault(); 287 return false; 288 } 289 290 function swipeTouchEnd(event) { 291 swipeCleanup(); 292 if (moved) event.preventDefault(); 293 // The native code handles conversion of non-moves into clicks 294 var nextPos = swipeEdge(1); 295 var width = nextPos.x + nextPos.y; 296 var threshold = width / 6; // Minimum movement to trigger transition 297 var which = (movedTo.x + movedTo.y < -threshold ? -1 : 298 (movedTo.x + movedTo.y > threshold ? 1 : 0)); 299 if (which != 0 && !elts.ok(which < 0)) which = 0; 300 var endPos = swipeEdge(which); 301 var distance = 302 Math.abs(endPos.x - movedTo.x + endPos.y - movedTo.y); 303 var duration = Math.round(300 * distance / width); 304 elts.cur.addEventListener('transitionend', swipeTranEnd, false); 305 swipeMoveTo(endPos, (duration > 0 ? duration : 1)); 306 // "1" forces a callback to "swipeTranEnd" 307 var oldCur = elts.cur; 308 if (which < 0) { 309 elts.cur = elts.next; 310 elts.next = elts.prev; 311 elts.prev = oldCur; 312 } else if (which > 0) { 313 elts.cur = elts.prev; 314 elts.prev = elts.next; 315 elts.next = oldCur; 316 } 317 if (which != 0) elts.action(which < 0); 318 return false; 319 } 320 321 function swipeTranEnd(event) { 322 this.removeEventListener('transitionend', swipeTranEnd, false); 323 elts.prev.style.display = "none"; 324 elts.prev.innerHTML = ""; 325 elts.next.style.display = "none"; 326 elts.next.innerHTML = ""; 327 } 328 329 return elts; 330 } 331 332 333 // 334 // Button management 335 // 336 337 function button(id, action, initPhotoTip, folderTip) { 338 // Create an object managing the element "id" as a button. Assigns the 339 // object to "btns[id]". The object has methods "enable", "disable" and 340 // "hide", and a few properties (all except "photoTip" are read-only). 341 // 342 var elt = document.getElementById(id); 343 var className = elt.className; 344 345 function btnShow(btn) { 346 if (!btn.visible) btn.element.style.display = null; 347 btn.visible = true; 348 } 349 function btnHide() { 350 // Remove a button from the screen 351 // 352 if (this.visible) elt.style.display = "none"; 353 this.visible = false; 354 this.enabled = false; 355 } 356 function btnEnable() { 357 // Enable and show a button 358 // 359 if (!this.enabled) { 360 elt.className = className; 361 elt.onclick = action; 362 elt.title = (thisPage && thisPage.isPhoto ? this.photoTip : 363 folderTip); 364 } 365 this.enabled = true; 366 btnShow(this); 367 } 368 function btnDisable() { 369 // Disable and show a button 370 // 371 if (this.enabled || !this.visible) { 372 elt.className = className + " disabled"; 373 elt.onclick = null; 374 elt.title = "Disabled"; 375 } 376 this.enabled = false; 377 btnShow(this); 378 } 379 380 if (!folderTip) folderTip = initPhotoTip; 381 btns[id] = { 382 element: elt, 383 initPhotoTip: initPhotoTip, 384 photoTip: initPhotoTip, 385 enabled: true, 386 visible: true, 387 enable: btnEnable, 388 disable: btnDisable, 389 hide: btnHide 390 }; 391 btns[id].disable(); 392 } 393 394 function fixupZoomButtons() { 395 // Enable or disable zoom buttons 396 // 397 if (!thisPage || preloadServer) { 398 btns.shrink.disable(); 399 btns.magnify.disable(); 400 } else if (thisPage.isPhoto) { 401 if (mainZoomLevel > 1) { 402 btns.shrink.enable(); 403 } else { 404 btns.shrink.disable(); 405 } 406 var cur = swiper.cur; 407 if (cur.clientHeight * mainZoomLevel < thisPage.photoActualH || 408 cur.clientWidth * mainZoomLevel < thisPage.photoActualW) { 409 btns.magnify.enable(); 410 } else { 411 btns.magnify.disable(); 412 } 413 } else if (thisPage.photoPaths.length == 0) { 414 btns.shrink.disable(); 415 btns.magnify.disable(); 416 } else { 417 if (thumbShrinkLevel < 6) { 418 btns.shrink.enable(); 419 } else { 420 btns.shrink.disable(); 421 } 422 if (thumbShrinkLevel > 3) { 423 btns.magnify.enable(); 424 } else { 425 btns.magnify.disable(); 426 } 427 } 428 } 429 430 function fixupButtons() { 431 // Enable/disable/hide buttons as appropriate for current page 432 // 433 fixupZoomButtons(); 434 if (thisPage.sorting) { 435 btns.sortSave.enable(); 436 btns.sortRevert.enable(); 437 } else { 438 btns.sortSave.hide(); 439 btns.sortRevert.hide(); 440 } 441 if (thisPage.sorting) { 442 btns.parentBtns.disable(); 443 btns.next.disable(); 444 btns.prev.disable(); 445 btns.skip.disable(); 446 btns.edit.hide(); 447 btns.title.disable(); 448 btns.raw.hide(); 449 btns.arrowT.hide(); 450 btns.arrowL.hide(); 451 btns.arrowR.hide(); 452 } else if (preloadServer) { 453 btns.parentBtns.disable(); 454 btns.next.disable(); 455 btns.prev.disable(); 456 btns.skip.disable(); 457 btns.edit.disable(); 458 btns.title.disable(); 459 btns.raw.disable(); 460 btns.arrowT.hide(); 461 btns.arrowL.hide(); 462 btns.arrowR.hide(); 463 } else { 464 if (thisPage.lastParent == "") { 465 btns.parentBtns.disable(); 466 } else { 467 btns.parentBtns.enable(); 468 } 469 if (thisPage.nextImage != "" || thisPage.isPhoto) { 470 btns.next.enable(); 471 } else { 472 btns.next.disable(); 473 } 474 if (thisPage.prevImage != "" || thisPage.isPhoto) { 475 btns.prev.enable(); 476 } else { 477 btns.prev.disable(); 478 } 479 var hasSub = thisPage.hasFolders; 480 if (thisPage.isPhoto) { 481 btns.skip.enable(); 482 } else if (thisPage.skipImage != "" && 483 ((autoRoot && autoRoot != thisPage.lastParent) || hasSub)) { 484 btns.skip.enable(); 485 } else { 486 btns.skip.disable(); 487 } 488 btns.edit.enable(); 489 btns.title.enable(); 490 if (thisPage.isPhoto && user != "") { 491 btns.raw.enable(); 492 } else { 493 btns.raw.disable(); 494 } 495 if (thisPage.isPhoto) { 496 btns.arrowT.enable(); 497 btns.arrowL.enable(); 498 btns.arrowR.enable(); 499 } else { 500 btns.arrowT.hide(); 501 btns.arrowL.hide(); 502 btns.arrowR.hide(); 503 } 504 } 505 if (thisPage.sorting) { 506 btns.auto.disable(); 507 btns.pause.hide(); 508 btns.resume.hide(); 509 btns.stop.hide(); 510 } else if (timer) { 511 btns.auto.hide(); 512 if (autoRoot != thisPage.lastParent && !preloadServer) { 513 btns.pause.enable(); 514 btns.stop.hide(); 515 } else { 516 btns.pause.hide(); 517 btns.stop.enable(); 518 } 519 btns.resume.hide(); 520 } else if (autoRoot) { 521 btns.auto.hide(); 522 btns.pause.hide(); 523 if (thisPage.isPhoto) { 524 btns.resume.enable(); 525 } else { 526 btns.resume.disable(); 527 } 528 btns.stop.enable(); 529 } else { 530 if (thisPage.autoImage != "") { 531 btns.auto.enable(); 532 } else { 533 btns.auto.disable(); 534 } 535 btns.pause.hide(); 536 btns.resume.hide(); 537 btns.stop.hide(); 538 } 539 } 540 541 542 // 543 // Progress bar dialog 544 // 545 546 var progressBar = (function() { // inline call of anonymous function 547 // 548 // This is purely for user entertainment: it doesn't measure progress. 549 550 var pgDlog = null; 551 var pgWidget = null; 552 var pgTimer = null; 553 var pgWidth = 0; 554 555 function stepProgressBar() { 556 var maxWidth = pgWidget.parentNode.offsetWidth; 557 pgWidth += (pgWidth <= 20 ? 4 : 558 (pgWidth <= 40 ? 3 : 559 (pgWidth <= 60 ? 2 : 1))); 560 if (pgWidth > maxWidth-4) pgWidth = 0; 561 pgWidget.style.width = "" + pgWidth + "px"; 562 } 563 564 function startProgressBar() { 565 if (!pgDlog) pgDlog = document.getElementById("reading"); 566 if (!pgWidget) pgWidget = document.getElementById("readingWidget"); 567 if (!pgTimer) { 568 pgDlog.style.display = "block"; 569 pgWidget.style.width = "4px"; // amount initially visible 570 pgWidth = 4; 571 pgTimer = setInterval(stepProgressBar, 500); 572 } 573 } 574 575 function stopProgressBar() { 576 if (pgTimer) { 577 clearInterval(pgTimer); 578 pgTimer = null; 579 pgDlog.style.display = "none"; 580 } 581 } 582 583 return { 584 start: startProgressBar, 585 stop: stopProgressBar 586 }; 587 588 }()); // end of inline call of anonymous function 589 590 function noteReading() { 591 // Start the progress dialog and disable buttons as appropriate 592 // 593 if (!preloadServer) progressBar.start(); 594 btns.next.disable(); 595 btns.prev.disable(); 596 btns.skip.disable(); 597 if (btns.auto.visible) btns.auto.disable(); 598 btns.edit.disable(); 599 btns.title.disable(); 600 if (dlogs.editForm.style.display == 'block') { 601 editTitle.blur(); 602 editInner.style.visibility = "hidden"; 603 } 604 } 605 606 607 // 608 // Scaling the main image 609 // 610 611 function scaleMainImage() { 612 // Adjust the position of the button bars to match their intended 613 // visibility, and adjust the sizing of the main image to match the 614 // button bar visibility and the current zoom state. 615 // 616 var showButtons = (!thisPage.isPhoto || buttonsVisible); 617 var showTitle = (!thisPage.isPhoto || titleVisible); 618 var topBar = document.getElementById("topStuff"); 619 var bottomBar = document.getElementById("bottomStuff"); 620 var topH = topBar.offsetHeight; 621 var bottomH = bottomBar.offsetHeight; 622 var topPx = "" + (showButtons ? topH : 0) + "px"; 623 var bottomPx = "" + (showTitle ? bottomH : 0) + "px"; 624 var sidePx = (showButtons ? null : "0px"); 625 swiper.cur.style.transition = "750ms"; 626 swiper.cur.style.top = topPx; 627 swiper.prev.style.top = topPx; 628 swiper.next.style.top = topPx; 629 btns.arrowT.element.style.top = topPx; 630 btns.arrowL.element.style.top = topPx; 631 btns.arrowR.element.style.top = topPx; 632 swiper.cur.style.bottom = bottomPx; 633 swiper.prev.style.bottom = bottomPx; 634 swiper.next.style.bottom = bottomPx; 635 btns.arrowL.element.style.bottom = bottomPx; 636 btns.arrowR.element.style.bottom = bottomPx; 637 topBar.style.top = "" + (showButtons ? 0 : -topH) + "px"; 638 bottomBar.style.bottom = "" + (showTitle ? 0 : -bottomH) + "px"; 639 btns.title.element.style.left = sidePx; 640 btns.title.element.style.right = sidePx; 641 btns.title.element.style.backgroundColor = 642 (showButtons ? null : "inherit"); 643 btns.title.element.style.color = 644 (showButtons ? null : "inherit"); 645 if (thisPage.isPhoto && !preloadServer) { 646 var mainImg = document.getElementById(thisPage.path); 647 var scale = "" + mainZoomLevel + "00%"; 648 mainImg.style.maxWidth = scale; 649 mainImg.style.maxHeight = scale; 650 } 651 } 652 653 function centerIfZoomed() { 654 // When loading a new photo, or re-zooming thisPage, center the scroll 655 // 656 if (thisPage && thisPage.isPhoto && !preloadServer) { 657 var mainImg = document.getElementById(thisPage.path); 658 var container = swiper.cur; 659 var height = container.scrollHeight - container.clientHeight; 660 var width = container.scrollWidth - container.clientWidth; 661 container.scrollTop = Math.round(height / 2); 662 container.scrollLeft = Math.round(width / 2); 663 mainImg.style.visibility = "visible"; 664 } 665 } 666 667 function photoZoom(bigger) { 668 // Magnify or shrink the main image or the photo thumbnails 669 // 670 if (timer) pauseOrManual(); 671 if (thisPage.isPhoto) { 672 // Fix up excessive zoom levels left over from resizing 673 var cH = swiper.cur.clientHeight; 674 var cW = swiper.cur.clientWidth; 675 var pH = thisPage.photoActualH; 676 var pW = thisPage.photoActualW; 677 while (mainZoomLevel > 1 && 678 cH * mainZoomLevel >= pH * 2 && cW * mainZoomLevel >= pW * 2) { 679 mainZoomLevel /= 2; 680 } 681 mainZoomLevel = (bigger ? mainZoomLevel * 2 : 682 (mainZoomLevel > 1 ? mainZoomLevel / 2 : 1)); 683 fixupZoomButtons(); 684 scaleMainImage(); 685 centerIfZoomed(); 686 } else { 687 if (bigger ? thumbShrinkLevel > 3 : thumbShrinkLevel < 6) { 688 thumbShrinkLevel = (bigger ? thumbShrinkLevel - 1 : 689 thumbShrinkLevel + 1); 690 thisPage = buildPage(thisPage.doc); 691 displayPage(); 692 } 693 } 694 return false; 695 } 696 697 698 // 699 // Prefetching Images 700 // 701 702 var imagePreload = (function() { // inline call of anonymous function 703 // 704 // Returns an object with two methods: "enqueue" requests preload of an 705 // image of the given source URL. Requests are queued and handled 706 // sequentially. The "cancel" method erases the request queue. 707 708 var imgQueue = null; 709 var imgQueueTail = null; 710 var seq = 1; 711 712 function initiateLoad() { 713 var mySeq; 714 var prefetch = document.createElement('img'); 715 var handler = function() { stepPreload(mySeq); }; 716 prefetch.onload = handler; 717 prefetch.onerror = handler; 718 if (imgQueue) { 719 mySeq = imgQueue.seq; 720 prefetch.src = imgQueue.src; 721 if (prefetch.complete) stepPreload(mySeq); 722 } 723 } 724 725 function stepPreload(mySeq) { 726 // Called on completion of current load. Might get called twice, 727 // because of the relationship between "complete" and "onload". 728 // 729 if (imgQueue && imgQueue.seq == mySeq) { 730 // console.log("Loaded " + mySeq + ": " + imgQueue.src); 731 imgQueue = imgQueue.next; 732 // call "initiateLoad" asynchronously, to avoid recursive 733 // overflow if the "onload" event handler gets called in-line. 734 if (imgQueue) setTimeout(initiateLoad, 10); 735 } 736 } 737 738 function enqueuePreload(src) { 739 var item = {seq: seq++, src: src, next: null}; 740 if (imgQueue) { 741 imgQueueTail.next = item; 742 } else { 743 imgQueue = item; 744 initiateLoad(); 745 } 746 imgQueueTail = item; 747 } 748 749 function cancelPreload() { 750 if (imgQueue) { 751 imgQueue.next = null; 752 imgQueueTail = imgQueue; 753 } 754 } 755 756 return { 757 enqueue: enqueuePreload, 758 cancel: cancelPreload 759 } 760 761 }()); // end of inline call of anonymous function 762 763 function preloadImages(doc) { 764 // Arrange that relevant images for this page are fetched into the 765 // browser's cache. 766 // 767 if (preloadServer) { 768 // don't preload any images 769 } else if (doc.nodeName == "photo") { 770 // main photo 771 imagePreload.enqueue(doc.getAttribute("src")); 772 } else { 773 // sub-folder thumbnails 774 var fChildren = doc.getElementsByTagName("folder"); 775 for (var i = 0; i < fChildren.length; i++) { 776 var fChild = fChildren[i]; 777 imagePreload.enqueue(fChild.getAttribute("src")); 778 } 779 // photo thumbnails 780 var pChildren = doc.getElementsByTagName("photo"); 781 for (var i = 0; i < pChildren.length; i++) { 782 var pChild = pChildren[i]; 783 imagePreload.enqueue(pChild.getAttribute("src")); 784 } 785 } 786 } 787 788 789 // 790 // Page interpretation 791 // 792 793 function getTitle(node) { 794 // Return the value of the "title" child of given XML node 795 // 796 var titles = node.getElementsByTagName("title"); 797 var child = titles[0].firstChild; 798 // The title is the child, but there's no child if the title was empty 799 return (child ? child.nodeValue : ""); 800 } 801 802 function buildPage(doc) { 803 // Construct and return a page object based on the given XML 804 // 805 var newPage = new Object(); 806 newPage.doc = doc; 807 newPage.sorting = false; 808 newPage.path = doc.getAttribute("path"); 809 newPage.title = getTitle(doc); 810 newPage.nextImage = doc.getAttribute("next"); 811 newPage.prevImage = doc.getAttribute("prev"); 812 newPage.skipImage = doc.getAttribute("skip"); 813 newPage.isThumb = (doc.getAttribute("thumb") == "Y"); 814 var parents = doc.getElementsByTagName("parent"); 815 if (parents.length == 0) { 816 newPage.parentTxt = "&nbsp;"; // non-empty to force line-height 817 newPage.lastParent = ""; 818 } else { 819 var lastP = parents[parents.length-1]; 820 var lastPTitle = getTitle(lastP); 821 var lastPPath = lastP.getAttribute("path"); 822 newPage.parentTxt = 823 "<span id=upTo>Up to: </span>" + adbab.htmlspecials(lastPTitle); 824 newPage.lastParent = lastPPath; 825 } 826 var thumbnail = doc.getElementsByTagName("thumbnail"); 827 newPage.thumbnailPath = thumbnail[0].getAttribute("src"); 828 if (doc.nodeName == "photo") { 829 newPage.isPhoto = true; 830 newPage.autoRoot = newPage.lastParent; 831 newPage.autoImage = // enable auto iff next is in this folder 832 (newPage.autoRoot == "." || 833 newPage.nextImage.indexOf(newPage.autoRoot) == 0 ? 834 newPage.nextImage : ""); 835 newPage.rawPath = doc.getAttribute("raw"); 836 var width = parseInt(doc.getAttribute("width")); 837 var height = parseInt(doc.getAttribute("height")); 838 var src = doc.getAttribute("src"); 839 var size = doc.getAttribute("size"); 840 var date = doc.getAttribute("date"); 841 var exposure = doc.getAttribute("exposure"); 842 var model = doc.getAttribute("model"); 843 newPage.photoActualW = width; 844 newPage.photoActualH = height; 845 newPage.hasFolders = false; 846 var temp = mainFrag.replace(/<#SRC>/, src); 847 temp = temp.replace(/<#ID>/g, newPage.path); 848 newPage.html = temp; 849 newPage.rawSize = size; 850 newPage.commentary = (date ? adbab.htmlspecials(date) : "&nbsp;") + 851 "<br>" + 852 (model ? adbab.htmlspecials(model + ", ") : "") + 853 (exposure ? adbab.htmlspecials(exposure) : ""); 854 } else { 855 // folder 856 newPage.isPhoto = false; 857 newPage.autoRoot = newPage.path; 858 newPage.autoImage = doc.getAttribute("first"); 859 var content = "<div id=\"" + newPage.path + "\" class=listing>"; 860 // 861 // First the sub-folders. We assemble HTML as a sequence of 862 // "folderFrag", which is a DIV containing the folder thumbnail and 863 // its title. The assemblage is embedded in "<DIV 864 // class=folderList>", which uses multi-column layout (but single 865 // column in old browsers). 866 // 867 var fChildren = doc.getElementsByTagName("folder"); 868 var folderCount = fChildren.length; 869 newPage.hasFolders = (folderCount > 0); 870 if (folderCount > 0) { 871 content += '<div class="folderList folderList'+ 872 (folderCount <= 5 ? '1' : 873 (folderCount <= 10 ? '2' : 874 (folderCount <= 15 ? '3' : '4'))) + '">'; 875 for (var i = 0; i < folderCount; i++) { 876 var subFolder = fChildren[i]; 877 var chpath = subFolder.getAttribute("path"); 878 var chTitle = getTitle(subFolder); 879 var width = subFolder.getAttribute("width"); 880 var height = subFolder.getAttribute("height"); 881 var src = subFolder.getAttribute("src"); 882 var temp = folderFrag.replace(/<#PATH>/g, chpath); 883 temp = temp.replace(/<#SRC>/, src); 884 temp = temp.replace(/<#TITLE>/g, 885 adbab.htmlspecials(chTitle)); 886 content += temp; 887 } 888 content += "</div>"; // end of <div class=folderList> 889 } 890 // 891 // Next the child photographs. We assemble HTML as a sequence 892 // of "photoFrag", which is an inline-block. The browser will 893 // do line-wrapping as appropriate. 894 // 895 var thumbShrink = (thumbShrinkLevel / 3) * thumbDownscale; 896 var perPhotoW = Math.floor( 897 parseInt(doc.getAttribute("photoMaxW")) / thumbShrink); 898 var photoMaxH = Math.floor( 899 parseInt(doc.getAttribute("photoMaxH")) / thumbShrink); 900 var pChildren = doc.getElementsByTagName("photo"); 901 var photoCount = pChildren.length; 902 newPage.photoPaths = []; 903 newPage.photoPathFromID = {}; 904 content += '<div class=photoList>'; 905 for (var i = 0; i < photoCount; i++) { 906 var sub = pChildren[i]; 907 var chpath = sub.getAttribute("path"); 908 var chID = "thumb-" + chpath; 909 var chTitle = getTitle(sub); 910 var width = parseInt(sub.getAttribute("width")); 911 var height = parseInt(sub.getAttribute("height")); 912 var chDate = sub.getAttribute("date"); 913 var chExposure = sub.getAttribute("exposure"); 914 var chModel = sub.getAttribute("model"); 915 width = Math.floor(width / thumbShrink); 916 height = Math.floor(height / thumbShrink); 917 var lMargin = Math.floor((perPhotoW - width) / 2); 918 var rMargin = perPhotoW - width - lMargin; 919 var src = sub.getAttribute("src"); 920 var temp = photoFrag.replace(/<#PATH>/g, chpath); 921 temp = temp.replace(/<#ID>/, chID); 922 temp = temp.replace(/<#TITLE>/g, adbab.htmlspecials( 923 (chTitle ? chTitle : "(untitled)") + 924 (chDate ? "\n" + chDate : "") + 925 // (chExposure ? "\n" + chExposure : "") + 926 (chModel ? "\n" + chModel : ""))); 927 temp = temp.replace(/<#WIDTH>/, width); 928 temp = temp.replace(/<#HEIGHT>/, height); 929 temp = temp.replace(/<#SRC>/, src); 930 temp = temp.replace(/<#HPAD>/g, photoMaxH); 931 temp = temp.replace(/<#LPAD>/, lMargin); 932 temp = temp.replace(/<#RPAD>/, rMargin); 933 temp = temp.replace(/<#COMMENT>/, ""); 934 content += temp; 935 newPage.photoPaths.push(chpath); 936 newPage.photoPathFromID[chID] = chpath; 937 } 938 var listPad = photoPadFrag.replace(/<#WIDTH>/, perPhotoW); 939 for (var i = 0; i < 10; i++) content += listPad; 940 content += '</div>'; // end of <div class=photoList> 941 content += '</div>'; // end of <div class=listing> 942 newPage.html = content; 943 newPage.photoCount = photoCount; 944 newPage.commentary = 945 (folderCount > 0 ? "" + folderCount + " folders" : "&nbsp;") + 946 (folderCount > 0 && photoCount > 0 ? " and<br>" : "") + 947 (photoCount > 0 ? "" + photoCount + " photographs" : "&nbsp;"); 948 } 949 return newPage; 950 } 951 952 function displayPage() { 953 // Update the display based on the current page attributes 954 // 955 if (!preloadServer && history && history.replaceState) { 956 var stateObj = {}; 957 var hashValue = thisPage.path; 958 history.replaceState(stateObj, "", 959 (hashValue == "." ? "." : "#" + hashValue)); 960 } // rapid use x100 triggers a spurious security error in Safari 9.1 961 if (preloadServer) { 962 var temp = preloadFrag.replace(/<#PATH>/g, 963 adbab.htmlspecials(thisPage.path)); 964 swiper.cur.innerHTML = temp.replace(/<#TITLE>/g, 965 adbab.htmlspecials(thisPage.title)); 966 } else { 967 swiper.cur.innerHTML = thisPage.html; 968 } 969 swiper.cur.scrollTop = 0; 970 btns.raw.photoTip = btns.raw.initPhotoTip + 971 (thisPage.rawSize ? ":\n" + thisPage.rawSize : ""); 972 btns.raw.disable(); // to refresh photoTip in "fixupButtons" 973 var commentary = document.getElementById("commentary"); 974 commentary.innerHTML = thisPage.commentary; 975 commentary.title = 976 decodeURIComponent(thisPage.path).replace(/.*\//, ''); 977 btns.title.element.innerHTML = (thisPage.title == "" ? "&nbsp;" : 978 adbab.htmlspecials(thisPage.title)); 979 btns.parentBtns.element.innerHTML = thisPage.parentTxt; 980 btns.parentBtns.disable(); 981 if (thisPage.lastParent != "") btns.parentBtns.enable(); 982 editTitle.value = thisPage.title; 983 editThumb.checked = thisPage.isThumb; 984 editThumb.disabled = (thisPage.lastParent == ""); 985 if (dlogs.editForm.style.display == 'block') { 986 editInner.style.visibility = "visible"; 987 editTitle.focus(); 988 editTitle.select(); 989 } 990 var editThumbnail = document.getElementById("editThumbnail"); 991 editThumbnail.src = thisPage.thumbnailPath; 992 scaleMainImage(); 993 progressBar.stop(); 994 fixupButtons(); 995 function imgLoaded() { 996 progressBar.stop(); 997 return centerIfZoomed(); 998 } 999 if (!preloadServer && thisPage.isPhoto) { 1000 // Call centerIfZoomed, after the main image has loaded. 1001 var mainImg = document.getElementById(thisPage.path); 1002 mainImg.onload = imgLoaded; 1003 if (mainImg.complete) { 1004 centerIfZoomed(); 1005 } else { 1006 progressBar.start(); 1007 } 1008 } 1009 } 1010 1011 1012 // 1013 // Page access 1014 // 1015 1016 var fetching = {}; // paths currently being fetched from server 1017 1018 function checkDoc(doc, expected) { 1019 // Verify that an XMLHttp result has the correct tag name 1020 // 1021 if (!doc || doc.nodeName != expected) { 1022 // If !doc, then initiateXMLHttp has already done an alert. 1023 if (doc) alert('Unknown result, nodeName="' + doc.nodeName +'"'); 1024 imagePreload.cancel(); 1025 fetching = {}; 1026 if (thisPage) displayPage(); 1027 return false; 1028 } else { 1029 return true; 1030 } 1031 } 1032 1033 function eraseCaches() { 1034 // Set up new, clean, caches 1035 // 1036 cacheP = new adbab.Cache(50); 1037 cacheF = new adbab.Cache(50); 1038 } 1039 1040 function getXML(path, jumpTo) { 1041 // Fetch XML for given path. If it's already in cache, return the XML; 1042 // if it's already being fetched, do nothing, otherwise initiate fetch 1043 // "path" was urlencoded by the server-side script. 1044 // 1045 // Iff "jumpTo", arrange to display the page when it arrives. 1046 // 1047 var requestedPath = path; 1048 function photoFolderCallback(doc) { 1049 // Completion procedure for XMLHttp 1050 // 1051 if ((doc && doc.nodeName == "photo") || checkDoc("folder")) { 1052 var gotPath = doc.getAttribute("path"); 1053 var thisCache = (doc.nodeName == "photo" ? cacheP : cacheF); 1054 thisCache.write(gotPath, doc); 1055 if (jumpTarget == requestedPath) { 1056 // Note that the server can return a page with a different 1057 // path, for example if the one we requested doesn't exist 1058 jumpTarget = null; 1059 loadPage(doc); 1060 } 1061 fetching[requestedPath] = false; 1062 // console.log("Received: " + gotPath); 1063 preloadImages(doc); 1064 } 1065 } 1066 1067 var cachedXML = cacheP.read(requestedPath); 1068 if (!cachedXML) cachedXML = cacheF.read(requestedPath); 1069 if (cachedXML) { 1070 if (jumpTo) jumpTarget = null; // cancel any in-progress jumpTarget 1071 preloadImages(cachedXML); 1072 return cachedXML; 1073 } else { 1074 if (jumpTo) { 1075 jumpTarget = requestedPath; 1076 noteReading(); 1077 } 1078 if (!fetching[requestedPath]) { 1079 fetching[requestedPath] = true; 1080 adbab.initiateXMLHttp(photoFolderCallback, 1081 "photos.php?op=xml&path=" + requestedPath); 1082 } 1083 return false; 1084 } 1085 } 1086 1087 function loadPage(doc) { 1088 // Load the given page's XML into the window. 1089 // Also initiate page prefetching, and refresh auto-display timer 1090 // 1091 thisPage = buildPage(doc); 1092 imagePreload.cancel(); 1093 fetching = {}; // abandon previous page prefetching 1094 if (thisPage.path == autoRoot) manual(); 1095 displayPage(); 1096 if (!preloadServer) { 1097 // pre-fetch nearby pages 1098 if (thisPage.autoImage != "") getXML(thisPage.autoImage, false); 1099 if (thisPage.nextImage != "") getXML(thisPage.nextImage, false); 1100 if (thisPage.prevImage != "") getXML(thisPage.prevImage, false); 1101 // if (thisPage.lastParent != "") getXML(thisPage.lastParent, false); 1102 } 1103 if (timer) timer = 1104 window.setTimeout(doNext, (preloadServer ? 50 : autoInterval)); 1105 } 1106 1107 function jumpTo(path) { 1108 // Jump to given path 1109 // 1110 // Disabled when sorting, to give feedback if user clicks on a photo 1111 // thumbnail or the arrow keys while in sorting mode. 1112 // 1113 if (timer) clearTimeout(timer); 1114 if (thisPage && thisPage.sorting) { 1115 alert("Sorting: click \"Save\" or \"Revert\""); 1116 } else if (path != "") { 1117 var cached = getXML(path, true); 1118 if (cached) { 1119 cacheHits++; 1120 loadPage(cached); 1121 } else { 1122 cacheMisses++; 1123 } 1124 } 1125 return false; 1126 } 1127 1128 function containedJump(candidate) { 1129 // Since nextImage/prevImage/skipImage provide a full sequential 1130 // enumeration of the entire album. we use this function so that 1131 // the doNext/doPrev/doSkip operations will not take us outside of our 1132 // containing folder (or outside of autoroot if auto-playing), and 1133 // instead will take us to our containing folder (or autoroot). Except 1134 // that if thisPage is a folder and we're not auto-playing, we do go to 1135 // the next/prev/skip album-wide folder. 1136 // 1137 var target = candidate; 1138 var container = (autoRoot ? autoRoot : 1139 (thisPage.isPhoto ? thisPage.lastParent : null)); 1140 if (container) { 1141 if (container == ".") { 1142 if (candidate == "") target = container; 1143 } else { 1144 if (candidate.indexOf(container) != 0) target = container; 1145 } 1146 } 1147 if (target != "") jumpTo(target); 1148 } 1149 1150 1151 // 1152 // The top-level subroutines, mostly invoked from the HTML page 1153 // 1154 1155 function doNext() { 1156 containedJump(thisPage.nextImage); 1157 return false; 1158 } 1159 1160 function doPrev() { 1161 containedJump(thisPage.prevImage); 1162 return false; 1163 } 1164 1165 function doSkip() { 1166 containedJump(thisPage.skipImage); 1167 return false; 1168 } 1169 1170 function startAuto() { 1171 // Internal subroutine for "auto" 1172 // 1173 jumpTo(thisPage.autoImage); 1174 } 1175 1176 function auto(event) { 1177 // Start automatic mode 1178 // 1179 autoRoot = thisPage.autoRoot; 1180 preloadServer = (event.shiftKey ? true : false); 1181 if (preloadServer) eraseCaches(); 1182 if (!timer) timer = window.setTimeout(startAuto, 50); 1183 btns.auto.disable(); 1184 btns.pause.hide(); 1185 btns.resume.hide(); 1186 btns.stop.hide(); 1187 return false; 1188 } 1189 1190 function pause() { 1191 // Pause auto-play without losing track of autoRoot, and enable resume. 1192 // 1193 if (timer) clearTimeout(timer); 1194 timer = null; 1195 btns.auto.hide(); 1196 btns.pause.hide(); 1197 btns.resume.enable(); 1198 btns.stop.enable(); 1199 return false; 1200 } 1201 1202 function resume() { 1203 // Continue after pause 1204 // 1205 if (!timer) timer = window.setTimeout(doNext, 50); 1206 btns.auto.hide(); 1207 btns.pause.enable(); 1208 btns.resume.hide(); 1209 btns.stop.hide(); 1210 return false; 1211 } 1212 1213 function manual() { 1214 // Stop automatic mode 1215 // 1216 if (timer) clearTimeout(timer); 1217 timer = null; 1218 var wasPreloading = preloadServer; 1219 if (preloadServer) eraseCaches(); 1220 autoRoot = null; 1221 preloadServer = false; 1222 btns.auto.enable(); 1223 btns.pause.hide(); 1224 btns.resume.hide(); 1225 btns.stop.hide(); 1226 if (wasPreloading && thisPage) jumpTo(thisPage.path); 1227 return false; 1228 } 1229 1230 function pauseOrManual() { 1231 // If playing within autoRoot, stop auto-play. Otherwise, pause it. 1232 // 1233 return (autoRoot == thisPage.lastParent ? manual() : pause()); 1234 } 1235 1236 function up() { 1237 // Go up one folder level 1238 // 1239 if (thisPage.lastParent != "") { 1240 if (timer) pauseOrManual(); 1241 jumpTo(thisPage.lastParent); 1242 } 1243 return false; 1244 } 1245 1246 function magnify() { 1247 return photoZoom(true); 1248 } 1249 1250 function shrink() { 1251 return photoZoom(false); 1252 } 1253 1254 function photoClick() { 1255 // Click inside a photograph 1256 // 1257 if (buttonsVisible) { 1258 buttonsVisible = false; 1259 } else if (titleVisible) { 1260 titleVisible = false; 1261 } else { 1262 buttonsVisible = true; 1263 titleVisible = true; 1264 } 1265 scaleMainImage(); 1266 return false; 1267 } 1268 1269 function openRaw() { 1270 // Open a new window with the raw image file 1271 // 1272 if (thisPage && thisPage.isPhoto) { 1273 window.open(thisPage.rawPath); 1274 } 1275 return false; 1276 } 1277 1278 function noteWindowSize() { 1279 // Adjust zoom choices and thumbDownscale according to new window size. 1280 // Intended to be fast with no side-effects, normally. 1281 // 1282 var tds = thumbDownscale; 1283 var wWidth = adbab.windowSize().x; 1284 thumbDownscale = (wWidth < 768 ? 2 : 1); 1285 if (thisPage) { 1286 if (thumbDownscale != tds && !thisPage.isPhoto) { 1287 thisPage = buildPage(thisPage.doc); 1288 displayPage(); // don't call if isPhoto; re-centers the scroll 1289 } else { 1290 fixupZoomButtons(); 1291 } 1292 } 1293 return false; 1294 } 1295 1296 function keydown(event) { 1297 // Called on a keydown event in the document 1298 // 1299 if (dlogs.editForm.style.display == "block") { 1300 return true; 1301 } else { 1302 var key = (event.code ? event.code : event.keyCode); 1303 if (event.altKey || event.ctrlKey || event.metaKey) { 1304 return true; 1305 } else if (key === "Escape" || key === 27) { 1306 if (autoRoot) return manual(); 1307 } else if (key === "Space" || key === 32) { 1308 if (autoRoot) { 1309 if (timer) { 1310 return pauseOrManual(); 1311 } else { 1312 return resume(); 1313 } 1314 } else { 1315 return auto(event); 1316 } 1317 } else if (key === "ArrowLeft" || key === 37) { 1318 return doPrev(); 1319 } else if (key === "ArrowUp" || key === 38) { 1320 return up(); 1321 } else if (key === "ArrowRight" || key === 39) { 1322 return (event.shiftKey ? doSkip() : doNext()); 1323 } else if (key === "ArrowDown" || key === 40) { 1324 if (thisPage && !thisPage.isPhoto) { 1325 if (thisPage.photoPaths.length > 0) { 1326 return jumpTo(thisPage.photoPaths[0]); 1327 } else { 1328 alert("There are no photos directly in this folder. " + 1329 "Use \"Next\" or right-arrow to view the first " + 1330 "sub-folder."); 1331 return false; 1332 } 1333 } 1334 } else if (key === "Equal" || key === 187) { 1335 return magnify(); 1336 } else if (key === "Minus" || key === 189) { 1337 return shrink(); 1338 } 1339 return true; 1340 } 1341 } 1342 1343 1344 // 1345 // Dialogs 1346 // 1347 1348 function closeDlogs() { 1349 // Close all dialog elements 1350 // 1351 for (var dlog in dlogs) { 1352 var style = dlogs[dlog].style; 1353 if (style.display != 'none') style.display = 'none'; 1354 } 1355 return false; 1356 } 1357 1358 function openDlog(dlogName) { 1359 // Open a pop-up dialog 1360 // 1361 var dlog = dlogs[dlogName]; 1362 closeDlogs(); 1363 dlog.style.display = 'block'; 1364 return false; 1365 } 1366 1367 function about() { 1368 // Open the "about" dialog 1369 // 1370 return openDlog('about'); 1371 } 1372 1373 function openEditDlog() { 1374 // Open the edit dialog (also used for "save" when sorting) 1375 // 1376 if (timer) pauseOrManual(); 1377 if (location.protocol != "https:") { 1378 alert("Switching to a secure connection, " + 1379 "to protect the secrecy of your password"); 1380 location.protocol = "https:"; 1381 } else { 1382 openDlog("editForm"); 1383 var editHeader = document.getElementById("editHeader"); 1384 var editUser = document.getElementById("editUser"); 1385 var editPwd = document.getElementById("editPwd"); 1386 var editTitleGroup = document.getElementById("editTitleGroup"); 1387 editHeader.innerHTML = (thisPage.sorting ? 1388 "Save the new sort order" : 1389 "Edit and save the title"); 1390 editTitleGroup.style.visibility = 1391 (thisPage.sorting ? "hidden" : "inherit"); 1392 editUser.value = user; 1393 if (user == "") { 1394 editUser.focus(); 1395 } else if (editPwd.value == "") { 1396 editPwd.focus(); 1397 } else { 1398 if (!thisPage.sorting) editTitle.focus(); 1399 } 1400 } 1401 return false; 1402 } 1403 1404 var editStayOpen; // leave the dialog up after save completion 1405 1406 function editSave(stayOpen) { 1407 // Save edited data: either the title or the sort order 1408 // 1409 var thisIsPhoto = thisPage.isPhoto; 1410 function saveCallback(doc) { 1411 // Completion procedure for XMLHttp 1412 // 1413 if (checkDoc(doc, "save")) { 1414 editSaveDone(doc.getAttribute("status"), 1415 doc.getAttribute("path"), 1416 thisIsPhoto); 1417 } 1418 } 1419 if (location.protocol != "https:") { 1420 alert("The save operation is allowed only over HTTPS, " + 1421 "to protect the secrecy of your password"); 1422 } else { 1423 editStayOpen = stayOpen; 1424 user = document.getElementById("editUser").value; 1425 var pwd = document.getElementById("editPwd").value; 1426 var argData; 1427 if (thisPage.sorting) { 1428 argData = "&sort="; 1429 for (var i = 0; i < thisPage.sortPaths.length; i++) { 1430 if (i != 0) argData += "\n"; 1431 argData += encodeURIComponent(thisPage.sortPaths[i]); 1432 } 1433 } else { 1434 argData = "&title=" + encodeURIComponent(editTitle.value); 1435 argData += "&thumb=" + 1436 (document.getElementById("editThumb").checked ? "Y" : "N"); 1437 } 1438 editInner.style.visibility = "hidden"; 1439 writing.style.display = 'block'; 1440 // all sorts of things might need to be re-evaluated 1441 eraseCaches(); 1442 var postData = "path=" + thisPage.path + "&op=xmlSave" + 1443 argData + 1444 "&user=" + encodeURIComponent(user) + 1445 "&pwd=" + encodeURIComponent(pwd); 1446 adbab.initiateXMLHttp(saveCallback, "photos.php", postData); 1447 } 1448 return false; 1449 } 1450 1451 function editSaveDone(status, path, isPhoto) { 1452 // Handle response to "save" operation 1453 // 1454 if (isPhoto) { 1455 cacheP.flush(path); 1456 } else { 1457 eraseCaches(); 1458 } 1459 editInner.style.visibility = "visible"; 1460 writing.style.display = 'none'; 1461 if (status == "ok") { 1462 endSorting(); 1463 if (editStayOpen) { 1464 doNext(); 1465 } else { 1466 closeDlogs(); 1467 jumpTo(thisPage.path); 1468 } 1469 } else { 1470 alert("Failed: " + status); 1471 } 1472 } 1473 1474 function startSorting() { 1475 // Move into modal "sorting" state, if needed 1476 // 1477 if (timer) pauseOrManual(); 1478 if (!thisPage.sorting) { 1479 thisPage.sorting = true; 1480 thisPage.sortPaths = thisPage.photoPaths.slice(); 1481 fixupButtons(); 1482 } 1483 } 1484 1485 function endSorting() { 1486 // Move out of modal "sorting" state 1487 // 1488 thisPage.sorting = false; 1489 thisPage.sortPaths = null; 1490 } 1491 1492 function sortOne(srceID, destID) { 1493 // Move srceID to just before destID, or to end if destID is 1494 // null. The caller updates the rendered page. Assumes srceID 1495 // and destID are different. 1496 // 1497 var sortPaths = thisPage.sortPaths; 1498 var srcePath = thisPage.photoPathFromID[srceID]; 1499 var destPath = (destID ? thisPage.photoPathFromID[destID] : null); 1500 var srceIndex = sortPaths.indexOf(srcePath); 1501 if (srceIndex < 0) alert("Source not in sortPaths array"); 1502 sortPaths.splice(srceIndex, 1); 1503 var destIndex = (destPath ? sortPaths.indexOf(destPath) : 1504 sortPaths.length); 1505 if (destIndex < 0) alert("Dest not in sortPaths array"); 1506 sortPaths.splice(destIndex, 0, srcePath); 1507 } 1508 1509 function sortRevert() { 1510 // Abandon the tentative sort order 1511 // 1512 if (confirm("Revert to the previously saved sort order?")) { 1513 closeDlogs(); // perhaps the "save" dialog was open 1514 endSorting(); 1515 displayPage(); 1516 } 1517 return false; 1518 } 1519 1520 1521 // 1522 // Methods for our "swiper" object 1523 // 1524 1525 function swipeOK(topLeft) { 1526 // "ok" method for our swiper 1527 // 1528 return (topLeft ? btns.next.enabled : btns.prev.enabled); 1529 1530 } 1531 1532 function swipeAction(topLeft) { 1533 // "action" method for our swiper 1534 // 1535 if (topLeft) { 1536 doNext(); 1537 } else { 1538 doPrev(); 1539 } 1540 } 1541 1542 1543 // 1544 // Drag-and-drop of photo thumbnails 1545 // 1546 1547 var thumbMime = "text/x-adbab-photo-thumbnail"; 1548 1549 function thumbDragStart(event, elt) { 1550 // Initiate drag-and-drop of the given element 1551 // 1552 // The element is the div.listingPhoto containing the thumbnail. 1553 // Sets the drag data to the tag's ID, with our MIME type. 1554 // 1555 var srce = elt; 1556 srce.ondragend = function thumbDragEnd() { 1557 srce.ondragend = null; 1558 srce.style.opacity = "1.0" 1559 return true; 1560 }; 1561 event.dataTransfer.setData(thumbMime, srce.id); 1562 event.dataTransfer.effectAllowed = "move"; 1563 srce.style.opacity = "0.5"; 1564 return true; 1565 } 1566 1567 var lastThumbDest = null; 1568 var lastThumbTarget = null; 1569 1570 function thumbDragEnter(event, elt, isLeft) { 1571 // Setup the drop target and its event handlers. 1572 // "isLeft" means "elt" is the drop target to the left of its IMG 1573 // 1574 var target = elt; 1575 var dest = target.parentNode; 1576 if (!isLeft) dest = dest.nextSibling; 1577 // "dest" is never null, because there's padding at the end of the list 1578 function thumbHighlight(yes) { 1579 var n = "dropTarget" + (yes ? "Highlight" : ""); 1580 var other = (isLeft ? 1581 (dest.previousSibling ? 1582 dest.previousSibling.lastChild : null) : 1583 dest.firstChild); 1584 target.className = n + (isLeft ? "L" : "R"); 1585 if (other) other.className = n + (isLeft ? "R" : "L"); 1586 } 1587 function thumbTerminate() { 1588 target.ondragover = null; 1589 target.ondragleave = null; 1590 target.ondrop = null; 1591 } 1592 function thumbDragOver(event) { 1593 return false; 1594 }; 1595 function thumbDragLeave(event) { 1596 if (target == lastThumbTarget || dest != lastThumbDest) { 1597 // dragleave can happen after next dragenter 1598 thumbHighlight(false); 1599 } 1600 thumbTerminate(); 1601 return true; 1602 }; 1603 function thumbDragDrop(event) { 1604 var srceID = event.dataTransfer.getData(thumbMime); 1605 thumbHighlight(false); 1606 thumbTerminate(); 1607 if (srceID != "") { 1608 var srce = document.getElementById(srceID); 1609 if (srce != dest) { 1610 startSorting(); 1611 sortOne(srceID, dest.id); 1612 srce.parentNode.removeChild(srce); 1613 dest.parentNode.insertBefore(srce, dest); 1614 } 1615 } 1616 return false; 1617 }; 1618 target.ondragover = thumbDragOver; 1619 target.ondragleave = thumbDragLeave; 1620 target.ondrop = thumbDragDrop; 1621 event.dataTransfer.dropEffect = "move"; 1622 thumbHighlight(true); 1623 lastThumbTarget = target; 1624 lastThumbDest = dest; 1625 return false; 1626 } 1627 1628 1629 // 1630 // Init 1631 // 1632 1633 function init() { 1634 if ("ontouchstart" in document.documentElement) { 1635 document.documentElement.className += " touch"; 1636 } else { 1637 document.documentElement.className += " noTouch"; 1638 } 1639 1640 dlogs.about = document.getElementById("about"); 1641 dlogs.details = document.getElementById("details"); 1642 dlogs.editForm = document.getElementById("editForm"); 1643 1644 writing = document.getElementById("writing"); 1645 editInner = document.getElementById("editInner"); 1646 editTitle = document.getElementById("editTitle"); 1647 editThumb = document.getElementById("editThumb"); 1648 1649 button("parentBtns", up, "Move to the parent folder"); 1650 button("shrink", shrink, "Shrink"); 1651 btns.shrink.hide(); 1652 button("magnify", magnify, "Magnify"); 1653 btns.magnify.hide(); 1654 button("auto", auto, 1655 "Play all the photos in this folder, from here onward", 1656 "Play all the photos in this folder, including " + 1657 "those in its sub-folders"); 1658 button("pause", pause, 1659 "Suspend the slideshow, remembering the starting position"); 1660 btns.pause.hide(); 1661 button("resume", resume, "Resume the slideshow"); 1662 btns.resume.hide(); 1663 button("stop", manual, "Stop the slideshow"); 1664 btns.stop.hide(); 1665 button("prev", doPrev, 1666 "Move to the previous photo", 1667 "Move to the previous folder"); 1668 button("next", doNext, 1669 "Move to the next photo", 1670 "Move to the next folder, including sub-folders"); 1671 button("skip", doSkip, 1672 "Skip the remainder of this folder", 1673 "Move to the next folder, skipping this folder's sub-folders"); 1674 button("edit", openEditDlog, 1675 "Edit this photo's title", 1676 "Edit this folder's title"); 1677 button("title", openEditDlog, 1678 "Edit this photo's title", 1679 "Edit this folder's title"); 1680 button("raw", openRaw, 1681 "Open the original full-resolution image"); 1682 btns.raw.disable(); 1683 btns.raw.initPhotoTip = btns.raw.photoTip; 1684 button("sortSave", openEditDlog, 1685 "Save the order to the server"); 1686 btns.sortSave.hide(); 1687 button("sortRevert", sortRevert, 1688 "Revert to the previously saved order"); 1689 btns.sortRevert.hide(); 1690 button("help", about, 1691 "Learn about this program"); 1692 btns.help.enable(); 1693 button("arrowT", up, "Move to the parent folder"); 1694 btns.arrowT.hide(); 1695 button("arrowL", doPrev, "Move to the previous photo"); 1696 btns.arrowL.hide(); 1697 button("arrowR", doNext, "Move to the next photo"); 1698 btns.arrowR.hide(); 1699 1700 swiper = swipeInit("contents1", "contents2", "contents3", false); 1701 swiper.ok = swipeOK; 1702 swiper.action = swipeAction; 1703 1704 window.onresize = noteWindowSize; 1705 document.onkeydown = keydown; 1706 1707 // Enable the UI 1708 document.getElementById("parentBtns").style.visibility = "visible"; 1709 document.getElementById("navigationBtns").style.visibility = "visible"; 1710 document.getElementById("privBtns").style.visibility = "visible"; 1711 1712 noteWindowSize(); 1713 1714 eraseCaches(); 1715 1716 var dest = (location.hash && location.hash != "" ? 1717 location.hash.substring(1) : 1718 "."); 1719 jumpTo(dest); 1720 }
End of listing